Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-06 18:12:15 +00:00
parent 22d47e2001
commit f16d40013b
55 changed files with 709 additions and 583 deletions

View File

@ -242,6 +242,10 @@
.if-observability-skip-e2e-jobs: &if-observability-skip-e2e-jobs
if: '$SKIP_GITLAB_OBSERVABILITY_BACKEND_TRIGGER || $GITLAB_OBSERVABILITY_BACKEND_PIPELINE_TRIGGER_TOKEN == null || $GITLAB_OBSERVABILITY_BACKEND_TOKEN_FOR_CI_SCRIPTS == null'
.if-dev-internal-release: &if-dev-internal-release
if: '$CI_SERVER_HOST == "dev.gitlab.org" && $CI_PROJECT_PATH == "gitlab/gitlab-ee" && $CI_COMMIT_REF_NAME =~
/^[\d-]+-stable-ee$/ && $CI_COMMIT_TITLE =~ /^Update VERSION file.*internal/'
####################
# Changes patterns #
####################
@ -1098,6 +1102,7 @@
- <<: *if-dot-com-gitlab-org-schedule
variables:
ARCH: amd64,arm64
- <<: *if-dev-internal-release
- !reference [".build-images:rules:build-qa-image-merge-requests", rules]
- !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env", rules]
# Always build on stable branches to serve release-environments pipeline

View File

@ -1 +1 @@
73881b727a2a17e48dc365784b7f0370f9c62376
a2d87e9fa4447c81916079cce9beb1785dd1d76c

View File

@ -1,3 +1,4 @@
import createState from '~/batch_comments/stores/modules/batch_comments/state';
import * as types from '../stores/modules/batch_comments/mutation_types';
const processDraft = (draft) => ({
@ -75,4 +76,7 @@ export default {
const draft = this.drafts[draftIndex];
this.drafts.splice(draftIndex, 1, { ...draft, isEditing });
},
reset() {
Object.assign(this, createState());
},
};

View File

@ -1,4 +1,5 @@
import * as types from './mutation_types';
import createState from './state';
const processDraft = (draft) => ({
...draft,
@ -75,4 +76,7 @@ export default {
const draft = state.drafts[draftIndex];
state.drafts.splice(draftIndex, 1, { ...draft, isEditing });
},
reset(state) {
Object.assign(state, createState());
},
};

View File

@ -1,10 +1,12 @@
<script>
import { GlIntersperse } from '@gitlab/ui';
import NullPresenter from './null.vue';
export default {
name: 'CollectionPresenter',
components: {
NullPresenter,
GlIntersperse,
},
inject: ['presenter'],
props: {
@ -17,10 +19,10 @@ export default {
};
</script>
<template>
<span>
<gl-intersperse separator=" ">
<span v-for="(field, index) in data.nodes" :key="index" class="gl-inline-block gl-pr-2">
<component :is="presenter.forField(field)" />
</span>
<null-presenter v-if="!data.nodes.length" />
</span>
</gl-intersperse>
</template>

View File

@ -1,7 +1,5 @@
<script>
import { GlButton, GlButtonGroup, GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { throttle } from 'lodash';
import { __ } from '~/locale';
@ -13,6 +11,7 @@ import {
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { sanitize } from '~/lib/dompurify';
import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes';
import { useNotes } from '~/notes/store/legacy_notes';
import discussionNavigation from '../mixins/discussion_navigation';
export default {
@ -38,11 +37,10 @@ export default {
};
},
computed: {
...mapVuexGetters([
...mapState(useNotes, [
'getNoteableData',
'resolvableDiscussionsCount',
'unresolvedDiscussionsCount',
'allResolvableDiscussions',
]),
...mapState(useMrNotes, ['allVisibleDiscussionsExpanded']),
allResolved() {

View File

@ -5,12 +5,12 @@ import {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'pinia';
import { getLocationHash, doesHashExistInUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { useNotes } from '~/notes/store/legacy_notes';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
HISTORY_ONLY_FILTER_VALUE,
@ -59,7 +59,7 @@ export default {
};
},
computed: {
...mapGetters([
...mapState(useNotes, [
'getNotesDataByProp',
'timelineEnabled',
'isLoading',
@ -97,7 +97,7 @@ export default {
window.removeEventListener('hashchange', this.handleLocationHash);
},
methods: {
...mapActions([
...mapActions(useNotes, [
'filterDiscussion',
'setCommentsDisabled',
'setTargetNoteHash',

View File

@ -1,15 +1,15 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { mapState } from 'pinia';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, sprintf } from '~/locale';
import { useNotes } from '~/notes/store/legacy_notes';
export default {
directives: {
SafeHtml,
},
computed: {
...mapGetters(['getNotesDataByProp']),
...mapState(useNotes, ['getNotesDataByProp']),
registerLink() {
return this.getNotesDataByProp('registerPath');
},

View File

@ -1,10 +1,10 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { mapActions } from 'pinia';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { fetchPolicies } from '~/lib/graphql';
import { confidentialityQueries } from '~/sidebar/queries/constants';
import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
import { useNotes } from '~/notes/store/legacy_notes';
export default {
props: {
@ -50,7 +50,7 @@ export default {
});
},
methods: {
...mapActions(['setConfidentiality']),
...mapActions(useNotes, ['setConfidentiality']),
},
render() {
return null;

View File

@ -1,9 +1,9 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { s__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { useNotes } from '~/notes/store/legacy_notes';
import Tracking from '~/tracking';
import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants';
import notesEventHub from '../event_hub';
import { trackToggleTimelineView } from '../utils';
@ -17,19 +17,23 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
computed: {
...mapGetters(['timelineEnabled', 'sortDirection']),
...mapState(useNotes, ['isTimelineEnabled', 'discussionSortOrder']),
tooltip() {
return this.timelineEnabled ? timelineEnabledTooltip : timelineDisabledTooltip;
return this.isTimelineEnabled ? timelineEnabledTooltip : timelineDisabledTooltip;
},
trackingOptions() {
const { category, action, label, property, value } = trackToggleTimelineView(
this.isTimelineEnabled,
);
return [category, action, { label, property, value }];
},
},
methods: {
...mapActions(['setTimelineView', 'setDiscussionSortDirection']),
trackToggleTimelineView,
...mapActions(useNotes, ['setTimelineView', 'setDiscussionSortDirection']),
setSort() {
if (this.timelineEnabled && this.sortDirection !== DESC) {
if (this.isTimelineEnabled && this.discussionSortOrder !== DESC) {
this.setDiscussionSortDirection({ direction: DESC, persist: false });
}
},
@ -38,9 +42,10 @@ export default {
},
toggleTimeline(event) {
event.currentTarget.blur();
this.setTimelineView(!this.timelineEnabled);
this.setTimelineView(!this.isTimelineEnabled);
this.setSort();
this.setFilter();
Tracking.event(...this.trackingOptions);
},
},
};
@ -49,9 +54,8 @@ export default {
<template>
<gl-button
v-gl-tooltip
v-track-event="trackToggleTimelineView(timelineEnabled)"
icon="history"
:selected="timelineEnabled"
:selected="isTimelineEnabled"
:title="tooltip"
:aria-label="tooltip"
data-testid="timeline-toggle-button"

View File

@ -5,6 +5,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getLocationHash } from '~/lib/utils/url_utility';
import { pinia } from '~/pinia/instance';
import { useNotes } from '~/notes/store/legacy_notes';
import NotesApp from './components/notes_app.vue';
import { store } from './stores';
import { getNotesFilterData } from './utils/get_notes_filter_data';
@ -42,6 +43,8 @@ export default ({ editorAiActions = [] } = {}) => {
const notesData = JSON.parse(notesDataset.notesData);
useNotes().syncWith({ store });
store.dispatch('setNotesData', notesData);
store.dispatch('setNoteableData', noteableData);
store.dispatch('setUserData', currentUserData);

View File

@ -1,6 +1,7 @@
import { isEqual } from 'lodash';
import { STATUS_CLOSED, STATUS_REOPENED } from '~/issues/constants';
import { isInMRPage } from '~/lib/utils/common_utils';
import createState from '~/notes/stores/state';
import * as constants from '../../constants';
import * as types from '../../stores/mutation_types';
import * as utils from '../../stores/utils';
@ -450,4 +451,7 @@ export default {
[types.SET_MERGE_REQUEST_FILTERS](value) {
this.mergeRequestFilters = value;
},
reset() {
Object.assign(this, createState());
},
};

View File

@ -2,6 +2,7 @@ import { isEqual } from 'lodash';
import { STATUS_CLOSED, STATUS_REOPENED } from '~/issues/constants';
import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import createState from './state';
import * as types from './mutation_types';
import * as utils from './utils';
@ -450,4 +451,7 @@ export default {
[types.SET_MERGE_REQUEST_FILTERS](state, value) {
state.mergeRequestFilters = value;
},
reset(state) {
Object.assign(state, createState());
},
};

View File

@ -1,11 +1,13 @@
import Vue from 'vue';
import { createPinia, PiniaVuePlugin } from 'pinia';
import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia';
import { globalAccessorPlugin, syncWithVuex } from '~/pinia/plugins';
Vue.use(PiniaVuePlugin);
const pinia = createPinia();
setActivePinia(pinia);
pinia.use(syncWithVuex);
pinia.use(globalAccessorPlugin);

View File

@ -31,49 +31,52 @@ export const globalAccessorPlugin = (context) => {
*/
// use this only for component migration
export const syncWithVuex = (context) => {
const config = context.options.syncWith;
if (!config) {
return;
}
const {
store: vuexStore,
name: vuexName,
namespaced,
} = /** @type {{ store: VuexStore, [name]: string, [namespaced]: boolean }} */ config;
const getVuexState = vuexName ? () => vuexStore.state[vuexName] : () => vuexStore.state;
if (!isEqual(context.store.$state, getVuexState())) {
Object.entries(getVuexState()).forEach(([key, value]) => {
// we can't use store.$patch here because it will merge state, but we need to overwrite it
// eslint-disable-next-line no-param-reassign
context.store[key] = cloneDeep(value);
});
}
const syncWith = (config) => {
const {
store: vuexStore,
name: vuexName,
namespaced,
} = /** @type {{ store: VuexStore, [name]: string, [namespaced]: boolean }} */ config;
const getVuexState = vuexName ? () => vuexStore.state[vuexName] : () => vuexStore.state;
if (!isEqual(context.store.$state, getVuexState())) {
Object.entries(getVuexState()).forEach(([key, value]) => {
// we can't use store.$patch here because it will merge state, but we need to overwrite it
// eslint-disable-next-line no-param-reassign
context.store[key] = cloneDeep(value);
});
}
let committing = false;
let committing = false;
vuexStore.subscribe(
(mutation) => {
vuexStore.subscribe(
(mutation) => {
if (committing) return;
const { payload, type } = mutation;
const [prefixOrName, mutationName] = type.split('/');
committing = true;
if (!mutationName && prefixOrName in context.store) {
context.store[prefixOrName](cloneDeep(payload));
} else if (prefixOrName === vuexName && mutationName in context.store) {
context.store[mutationName](cloneDeep(payload));
}
committing = false;
},
{ prepend: true },
);
context.store.$onAction(({ name: mutationName, args }) => {
if (committing) return;
const { payload, type } = mutation;
const [prefixOrName, mutationName] = type.split('/');
const fullMutationName = namespaced ? `${vuexName}/${mutationName}` : mutationName;
// eslint-disable-next-line no-underscore-dangle
if (!(fullMutationName in vuexStore._mutations)) return;
committing = true;
if (!mutationName && prefixOrName in context.store) {
context.store[prefixOrName](cloneDeep(payload));
} else if (prefixOrName === vuexName && mutationName in context.store) {
context.store[mutationName](cloneDeep(payload));
}
vuexStore.commit(fullMutationName, ...cloneDeep(args));
committing = false;
},
{ prepend: true },
);
});
};
context.store.$onAction(({ name: mutationName, args }) => {
if (committing) return;
const fullMutationName = namespaced ? `${vuexName}/${mutationName}` : mutationName;
// eslint-disable-next-line no-underscore-dangle
if (!(fullMutationName in vuexStore._mutations)) return;
committing = true;
vuexStore.commit(fullMutationName, ...cloneDeep(args));
committing = false;
});
const initialConfig = context.options.syncWith;
if (initialConfig) syncWith(initialConfig);
return { syncWith };
};

View File

@ -5,7 +5,6 @@ module API
class GraphqlExplorerController < BaseActionController
include Gitlab::GonHelper
include WithPerformanceBar
include ViteCSP
def show
# We need gon to setup gon.relative_url_root which is used by our Apollo client

View File

@ -30,7 +30,6 @@ class ApplicationController < BaseActionController
include StrongPaginationParams
include Gitlab::HttpRouter::RuleContext
include Gitlab::HttpRouter::RuleMetrics
include ViteCSP
around_action :set_current_ip_address

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module ViteCSP
extend ActiveSupport::Concern
included do
content_security_policy_with_context do |p|
next unless helpers.vite_enabled?
next if p.directives.blank?
# We need both Websocket and HTTP URLs because Vite will attempt to ping
# the HTTP URL if the Websocket isn't available:
# https://github.com/vitejs/vite/blob/899d9b1d272b7057aafc6fa01570d40f288a473b/packages/vite/src/client/client.ts#L320-L327
hmr_ws_url = Gitlab::Utils.append_path(helpers.vite_hmr_ws_origin, 'vite-dev/')
http_path = Gitlab::Utils.append_path(helpers.vite_origin, 'vite-dev/')
# http_path is used for openInEditorHost feature
# https://devtools.vuejs.org/getting-started/open-in-editor#customize-request
p.connect_src(*(Array.wrap(p.directives['connect-src']) | [hmr_ws_url, http_path]))
p.worker_src(*(Array.wrap(p.directives['worker-src']) | [http_path]))
p.style_src(*(Array.wrap(p.directives['style-src']) | [http_path]))
p.font_src(*(Array.wrap(p.directives['font-src']) | [http_path]))
end
end
end

View File

@ -5,7 +5,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include PageLayoutHelper
include OauthApplications
include InitializesCurrentUserMode
include ViteCSP
# Defined by the `Doorkeeper::ApplicationsController` and is redundant as we call `authenticate_user!` below. Not
# defining or skipping this will result in a `403` response to all requests.

View File

@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include InitializesCurrentUserMode
include Gitlab::Utils::StrongMemoize
include RequestPayloadLogger
include ViteCSP
alias_method :auth_user, :current_user

View File

@ -2,7 +2,6 @@
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
include PageLayoutHelper
include ViteCSP
layout 'profile'

View File

@ -2,8 +2,6 @@
module Oauth
class DeviceAuthorizationsController < Doorkeeper::DeviceAuthorizationGrant::DeviceAuthorizationsController
include ViteCSP
layout 'minimal'
def index

View File

@ -13,7 +13,6 @@ class RegistrationsController < Devise::RegistrationsController
include Gitlab::RackLoadBalancingHelpers
include ::Gitlab::Utils::StrongMemoize
include Onboarding::Redirectable
include ViteCSP
layout 'devise'

View File

@ -8,15 +8,6 @@ module ViteHelper
Gitlab::Utils.to_boolean(ViteRuby.env['VITE_ENABLED'], default: false)
end
def vite_origin
ViteRuby.config.origin
end
def vite_hmr_ws_origin
protocol = ViteRuby.config.https ? 'wss' : 'ws'
"#{protocol}://#{ViteRuby.config.host_with_port}"
end
def vite_page_entrypoint_paths(custom_action_name = nil)
action_name = custom_action_name || controller.action_name
action = case action_name

View File

@ -5,7 +5,7 @@ def feature_mr?
end
def doc_path_to_url(path)
path.sub("doc/", "https://docs.gitlab.com/ee/").sub("index.md", "").sub(".md", "/")
path.sub("doc/", "https://docs.gitlab.com/").sub("index.md", "").sub(".md", "/")
end
docs_paths_to_review = helper.changes_by_category[:docs]
@ -15,7 +15,8 @@ docs_paths_to_review = helper.changes_by_category[:docs]
sections_with_no_tw_review = {
'doc/architecture' => [],
'doc/development' => [],
'doc/solutions' => []
'doc/solutions' => [],
'doc-locale' => []
}.freeze
# One exception to the exceptions above: Technical Writing docs should get a TW review.
@ -31,7 +32,7 @@ docs_paths_to_review.reject! do |doc|
end
SOLUTIONS_LABELS = %w[Solutions].freeze
DEVELOPMENT_LABELS = ['docs::improvement', 'development guidelines'].freeze
DEVELOPMENT_LABELS = ['development guidelines'].freeze
def add_labels(labels)
helper.labels_to_add.concat(%w[documentation type::maintenance maintenance::refactor] + labels)
@ -45,6 +46,10 @@ SOLUTIONS_MESSAGE = <<~MSG
This MR contains docs in the /doc/solutions directory and should be reviewed by a Solutions Architect approver. You do not need tech writer review.
MSG
LOCALIZATION_MESSAGE = <<~MSG
This MR contains files in the /doc-locale directory. These files are translations maintained through a separate process and should not be edited directly. If you are not part of the Localization team, please remove the changes to these files from your MR.
MSG
# For regular pages, prompt for a TW review
DOCS_UPDATE_SHORT_MESSAGE = <<~MSG
This merge request adds or changes documentation files and requires Technical Writing review. The review should happen before merge, but can be post-merge if the merge request is time sensitive.
@ -91,6 +96,8 @@ if sections_with_no_tw_review["doc/solutions"].any?
message(SOLUTIONS_MESSAGE)
end
message(LOCALIZATION_MESSAGE) if sections_with_no_tw_review["doc-locale"].any?
unless docs_paths_to_review.empty?
message(DOCS_UPDATE_SHORT_MESSAGE)
markdown(DOCS_UPDATE_LONG_MESSAGE)

View File

@ -207,7 +207,7 @@ including:
| Feature | Shortcut on Windows or Linux | Shortcut on macOS | Details |
|----------------------------------|-----------------------------------|------------------------------------------------------|---------|
| Keyboard navigation command list | <kbd>f1</kbd> | <kbd>f1</kbd> | A [list of commands](https://github.com/microsoft/monaco-editor/wiki/Monaco-Editor-Accessibility-Guide#keyboard-navigation) that make the editor easier to use without a mouse. |
| Keyboard navigation command list | <kbd>F1</kbd> | <kbd>F1</kbd> | A [list of commands](https://github.com/microsoft/monaco-editor/wiki/Monaco-Editor-Accessibility-Guide#keyboard-navigation) that make the editor easier to use without a mouse. |
| Tab trapping | <kbd>Control</kbd> + <kbd>m</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>m</kbd> | Enable [tab trapping](https://github.com/microsoft/monaco-editor/wiki/Monaco-Editor-Accessibility-Guide#tab-trapping) to go to the next focusable element on the page instead of inserting a tab character. |
## Troubleshooting

View File

@ -210,6 +210,16 @@ In the context of multi-agent workflows, a tool is a utility or application that
## AI Context Terminology
### Knowledge Graph
The [Knowledge Graph](https://gitlab.com/gitlab-org/rust/knowledge-graph) project aims to create a structured, queryable graph database from code repositories to power AI features and enhance developer productivity within GitLab.
Think of it like creating a detailed blueprint that shows which functions call other functions, how classes relate to each other, and where variables are used throughout the codebase. Instead of GitLab Duo having to read through thousands of files every time you ask it something, it can quickly navigate this pre-built map to give you better code suggestions, find related code snippets, or help debug issues. It gives Duo a much smarter way to understand your codebase so it can assist you more effectively with things like code reviews, refactoring, or finding where to make changes when you're working on a feature.
### One Parser (GitLab Code Parser)
The [GitLab Code Parser](https://gitlab.com/gitlab-org/code-creation/gitlab-code-parser#) establishes a single, efficient, and reliable static code analysis library. This library will serve as the foundation for diverse code intelligence features across GitLab, from server-side indexing (Knowledge Graph, Embeddings) to client-side analysis (Language Server, Web IDE). Initially scoped to AI and Editor Features.
### Advanced Context Resolver
Advanced context is a comprehensive set of code-related information extending

View File

@ -39,7 +39,12 @@ If you are a new customer in GitLab 18.0 or later, IDE features are automaticall
If you are a pre-existing customer from GitLab 17.11 or earlier, you must [turn on IDE features](../user/gitlab_duo/turn_on_off.md#change-gitlab-duo-core-availability) to start using GitLab Duo in your IDEs. No further action is needed.
Users assigned the [Guest role](../administration/guest_users.md) do not have access to GitLab Duo Core.
Users assigned the following roles have access to GitLab Duo Core:
- Reporter
- Developer
- Maintainer
- Owner
### GitLab Duo Core limits

View File

@ -123,12 +123,18 @@ Group permissions for [GitLab Duo](gitlab_duo/_index.md):
| Action | Non-member | Guest | Planner | Reporter | Developer | Maintainer | Owner | Notes |
| --------------------------------------------------------------------------------------------------------- | :--------: | :---: | :-----: | :------: | :-------: | :--------: | :---: | ----- |
| Use Duo features | | | | ✓ | ✓ | ✓ | ✓ | Requires [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats). |
| Use Duo features | | | | ✓ | ✓ | ✓ | ✓ | Requirements differ depending on if the user has GitLab Duo Core, Pro, or Enterprise. <sup>1</sup> |
| Configure [Duo feature availability](gitlab_duo/turn_on_off.md#for-a-group-or-subgroup) | | | | | | ✓ | ✓ | |
| Configure [GitLab Duo Self Hosted](../administration/gitlab_duo_self_hosted/configure_duo_features.md) | | | | | | | ✓ | |
| Enable [beta and experimental features](gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features) | | | | | | | ✓ | |
| Purchase [Duo seats](../subscriptions/subscription-add-ons.md#purchase-additional-gitlab-duo-seats) | | | | | | | ✓ | |
**Footnotes**
1. If the user has GitLab Duo Pro or Enterprise, the
[user must be assigned a seat to gain access to that Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats). If the user has GitLab Duo Core, there are
no other requirements.
### Groups group permissions
Group permissions for [group features](group/_index.md):

View File

@ -21,8 +21,8 @@ module Gitlab
GLAB_ENV_SET_WINDOWS = '$env:GITLAB_HOST = $env:CI_SERVER_URL'
GLAB_LOGIN_UNIX = 'glab auth login --job-token $CI_JOB_TOKEN --hostname $CI_SERVER_FQDN --api-protocol $CI_SERVER_PROTOCOL'
GLAB_LOGIN_WINDOWS = 'glab auth login --job-token $env:CI_JOB_TOKEN --hostname $env:CI_SERVER_FQDN --api-protocol $env:CI_SERVER_PROTOCOL'
GLAB_CREATE_UNIX = 'glab -R $CI_PROJECT_PATH release create'
GLAB_CREATE_WINDOWS = 'glab -R $env:CI_PROJECT_PATH release create'
GLAB_CREATE_UNIX = 'glab release create -R $CI_PROJECT_PATH'
GLAB_CREATE_WINDOWS = 'glab release create -R $env:CI_PROJECT_PATH'
GLAB_PUBLISH_TO_CATALOG_FLAG = '--publish-to-catalog' # enables publishing to the catalog after creating the release
GLAB_NO_UPDATE_FLAG = '--no-update' # disables updating the release if it already exists
GLAB_NO_CLOSE_MILESTONE_FLAG = '--no-close-milestone' # disables closing the milestone after creating the release

View File

@ -19,6 +19,7 @@ module Gitlab
def default_directives
directives = default_directives_defaults
allow_vite_dev_server(directives)
allow_development_tooling(directives)
allow_websocket_connections(directives)
allow_lfs(directives)
@ -73,6 +74,26 @@ module Gitlab
append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
end
def allow_vite_dev_server(directives)
return unless Rails.env.development? || Rails.env.test?
protocol = ViteRuby.config.https ? 'wss' : 'ws'
ws_origin = "#{protocol}://#{ViteRuby.config.host_with_port}"
# We need both Websocket and HTTP URLs because Vite will attempt to ping
# the HTTP URL if the Websocket isn't available:
# https://github.com/vitejs/vite/blob/899d9b1d272b7057aafc6fa01570d40f288a473b/packages/vite/src/client/client.ts#L320-L327
hmr_ws_url = Gitlab::Utils.append_path(ws_origin, 'vite-dev/')
http_path = Gitlab::Utils.append_path(ViteRuby.config.origin, 'vite-dev/')
# http_path is used for openInEditorHost feature
# https://devtools.vuejs.org/getting-started/open-in-editor#customize-request
append_to_directive(directives, 'connect_src', "#{hmr_ws_url} #{http_path}")
append_to_directive(directives, 'worker_src', http_path)
append_to_directive(directives, 'style_src', http_path)
append_to_directive(directives, 'font_src', http_path)
end
def allow_letter_opener(directives)
url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/')
append_to_directive(directives, 'frame_src', url)

View File

@ -107,3 +107,5 @@ module Sidebars
end
end
end
Sidebars::Groups::Menus::PackagesRegistriesMenu.prepend_mod_with('Sidebars::Groups::Menus::PackagesRegistriesMenu')

View File

@ -18,7 +18,8 @@ module Sidebars
def configure_menu_items
[
:packages_registry,
:container_registry
:container_registry,
:virtual_registry
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
end

View File

@ -49018,6 +49018,9 @@ msgstr ""
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
msgid "ProjectToolCoverageDetails|Manage configuration"
msgstr ""
msgid "ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again."
msgstr ""
@ -55317,7 +55320,7 @@ msgstr ""
msgid "SecurityInventory|This group doesn't have any subgroups."
msgstr ""
msgid "SecurityInventory|Tool coverage: %{coverage}%%"
msgid "SecurityInventory|Tool coverage: %{coverage}"
msgstr ""
msgid "SecurityInventory|View security coverage and vulnerabilities for all the projects in this group."
@ -64501,7 +64504,7 @@ msgstr ""
msgid "Tool Coverage"
msgstr ""
msgid "ToolCoverageDetails|Manage configuration"
msgid "ToolCoverage|Project coverage"
msgstr ""
msgid "Topic %{source_topic} was successfully merged into topic %{target_topic}."
@ -67760,6 +67763,9 @@ msgstr ""
msgid "Violation Details"
msgstr ""
msgid "Virtual registry"
msgstr ""
msgid "VirtualRegistries|API endpoints rate limit"
msgstr ""
@ -68031,9 +68037,21 @@ msgstr ""
msgid "Vulnerabilities|%{link_start}Download the export%{link_end}."
msgstr ""
msgid "Vulnerabilities|All records must be instances of Vulnerability"
msgstr ""
msgid "Vulnerabilities|Follow the link below to download the export."
msgstr ""
msgid "Vulnerabilities|Internal tracking failed: %{message}"
msgstr ""
msgid "Vulnerabilities|Missing required attributes: %{attributes}"
msgstr ""
msgid "Vulnerabilities|No valid vulnerabilities to track"
msgstr ""
msgid "Vulnerabilities|The vulnerabilities list was successfully exported for %{exportable}."
msgstr ""
@ -74193,6 +74211,9 @@ msgstr ""
msgid "objective"
msgstr ""
msgid "of"
msgstr ""
msgid "on or after"
msgstr ""

View File

@ -82,41 +82,41 @@
"@snowplow/browser-plugin-timezone": "^3.24.2",
"@snowplow/browser-tracker": "^3.24.2",
"@sourcegraph/code-host-integration": "0.0.95",
"@tiptap/core": "^2.13.0",
"@tiptap/extension-blockquote": "^2.13.0",
"@tiptap/extension-bold": "^2.13.0",
"@tiptap/extension-bubble-menu": "^2.13.0",
"@tiptap/extension-bullet-list": "^2.13.0",
"@tiptap/extension-code": "^2.13.0",
"@tiptap/extension-code-block": "^2.13.0",
"@tiptap/extension-code-block-lowlight": "^2.13.0",
"@tiptap/extension-document": "^2.13.0",
"@tiptap/extension-dropcursor": "^2.13.0",
"@tiptap/extension-gapcursor": "^2.13.0",
"@tiptap/extension-hard-break": "^2.13.0",
"@tiptap/extension-heading": "^2.13.0",
"@tiptap/extension-highlight": "^2.13.0",
"@tiptap/extension-history": "^2.13.0",
"@tiptap/extension-horizontal-rule": "^2.13.0",
"@tiptap/extension-image": "^2.13.0",
"@tiptap/extension-italic": "^2.13.0",
"@tiptap/extension-link": "^2.13.0",
"@tiptap/extension-list-item": "^2.13.0",
"@tiptap/extension-ordered-list": "^2.13.0",
"@tiptap/extension-paragraph": "^2.13.0",
"@tiptap/extension-strike": "^2.13.0",
"@tiptap/extension-subscript": "^2.13.0",
"@tiptap/extension-superscript": "^2.13.0",
"@tiptap/extension-table": "^2.13.0",
"@tiptap/extension-table-cell": "^2.13.0",
"@tiptap/extension-table-header": "^2.13.0",
"@tiptap/extension-table-row": "^2.13.0",
"@tiptap/extension-task-item": "^2.13.0",
"@tiptap/extension-task-list": "^2.13.0",
"@tiptap/extension-text": "^2.13.0",
"@tiptap/pm": "^2.13.0",
"@tiptap/suggestion": "^2.13.0",
"@tiptap/vue-2": "^2.13.0",
"@tiptap/core": "^2.14.0",
"@tiptap/extension-blockquote": "^2.14.0",
"@tiptap/extension-bold": "^2.14.0",
"@tiptap/extension-bubble-menu": "^2.14.0",
"@tiptap/extension-bullet-list": "^2.14.0",
"@tiptap/extension-code": "^2.14.0",
"@tiptap/extension-code-block": "^2.14.0",
"@tiptap/extension-code-block-lowlight": "^2.14.0",
"@tiptap/extension-document": "^2.14.0",
"@tiptap/extension-dropcursor": "^2.14.0",
"@tiptap/extension-gapcursor": "^2.14.0",
"@tiptap/extension-hard-break": "^2.14.0",
"@tiptap/extension-heading": "^2.14.0",
"@tiptap/extension-highlight": "^2.14.0",
"@tiptap/extension-history": "^2.14.0",
"@tiptap/extension-horizontal-rule": "^2.14.0",
"@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-italic": "^2.14.0",
"@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-list-item": "^2.14.0",
"@tiptap/extension-ordered-list": "^2.14.0",
"@tiptap/extension-paragraph": "^2.14.0",
"@tiptap/extension-strike": "^2.14.0",
"@tiptap/extension-subscript": "^2.14.0",
"@tiptap/extension-superscript": "^2.14.0",
"@tiptap/extension-table": "^2.14.0",
"@tiptap/extension-table-cell": "^2.14.0",
"@tiptap/extension-table-header": "^2.14.0",
"@tiptap/extension-table-row": "^2.14.0",
"@tiptap/extension-task-item": "^2.14.0",
"@tiptap/extension-task-list": "^2.14.0",
"@tiptap/extension-text": "^2.14.0",
"@tiptap/pm": "^2.14.0",
"@tiptap/suggestion": "^2.14.0",
"@tiptap/vue-2": "^2.14.0",
"@vue/apollo-components": "^4.0.0-beta.4",
"@vue/apollo-option": "^4.0.0-beta.4",
"apollo-upload-client": "15.0.0",

View File

@ -61,8 +61,7 @@ fi
# Then set DESTINATIONS to the content of VERSION file
if [[ "${QA_IMAGE_NAME}" == "gitlab-ee-qa" ]] && \
[[ "${CI_REGISTRY}" == "dev.gitlab.org:5005" ]] && \
[[ "$(version_file_content)" == *"internal"* ]] && \
[[ "${CI_COMMIT_TITLE}" == "Update VERSION files" ]]; then
[[ "$CI_COMMIT_TITLE" =~ ^Update\ VERSION\ file\ for.*internal ]]; then
QA_IMAGE_FOR_INTERNAL_RELEASE="${BASE_IMAGE_PATH}:$(version_file_content)"
DESTINATIONS="${DESTINATIONS} --tag ${QA_IMAGE_FOR_INTERNAL_RELEASE}"
fi

View File

@ -1,4 +1,6 @@
import { GlLabel } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import CollectionPresenter from '~/glql/components/presenters/collection.vue';
import LabelPresenter from '~/glql/components/presenters/label.vue';
import UserPresenter from '~/glql/components/presenters/user.vue';
@ -14,6 +16,11 @@ describe('CollectionPresenter', () => {
presenter: new Presenter(),
},
propsData: { data },
stubs: {
GlLabel: stubComponent(GlLabel, {
template: '<span>{{ title }}</span>',
}),
},
});
};
@ -30,8 +37,6 @@ describe('CollectionPresenter', () => {
expect(presenters.at(0).props('data')).toBe(mockData.nodes[0]);
expect(presenters.at(1).props('data')).toBe(mockData.nodes[1]);
expectedTexts.forEach((text) => {
expect(wrapper.text()).toContain(text);
});
expect(wrapper.text()).toEqual(expectedTexts.join(' '));
});
});

View File

@ -4,6 +4,8 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import waitForPromises from 'helpers/wait_for_promises';
import {
extendedWrapper,
@ -26,6 +28,9 @@ import notesModule from '~/notes/stores/modules';
import { sprintf } from '~/locale';
import { mockTracking } from 'helpers/tracking_helper';
import { detectAndConfirmSensitiveTokens } from '~/lib/utils/secret_detection';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@ -38,6 +43,7 @@ jest.mock('~/lib/utils/secret_detection', () => {
});
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('issue_comment_form component', () => {
useLocalStorageSpy();
@ -45,6 +51,7 @@ describe('issue_comment_form component', () => {
let trackingSpy;
let wrapper;
let axiosMock;
let pinia;
const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
@ -126,6 +133,7 @@ describe('issue_comment_form component', () => {
};
},
store,
pinia,
provide: {
glFeatures: features,
},
@ -134,6 +142,9 @@ describe('issue_comment_form component', () => {
};
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs();
useNotes();
axiosMock = new MockAdapter(axios);
trackingSpy = mockTracking(undefined, null, jest.spyOn);
detectAndConfirmSensitiveTokens.mockReturnValue(true);

View File

@ -1,43 +1,37 @@
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import * as types from '~/notes/stores/mutation_types';
import { createStore } from '~/mr_notes/stores';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes';
import { discussionMock, noteableDataMock, notesDataMock } from '../mock_data';
import { createDiscussionMock, noteableDataMock, notesDataMock } from '../mock_data';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('DiscussionCounter component', () => {
let store;
let pinia;
let wrapper;
const createComponent = (propsData) => {
wrapper = mount(DiscussionCounter, { store, pinia, propsData });
wrapper = mount(DiscussionCounter, { pinia, propsData });
};
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs();
useNotes();
useMrNotes();
window.mrTabs = {};
store = createStore();
store.dispatch('setNoteableData', {
useNotes().setNoteableData({
...noteableDataMock,
create_issue_to_resolve_discussions_path: '/test',
});
store.dispatch('setNotesData', notesDataMock);
useNotes().setNotesData(notesDataMock);
});
describe('has no discussions', () => {
@ -50,8 +44,10 @@ describe('DiscussionCounter component', () => {
describe('has no resolvable discussions', () => {
it('does not render', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
useNotes()[types.ADD_OR_UPDATE_DISCUSSIONS]([
{ ...createDiscussionMock(), resolvable: false },
]);
useNotes().updateResolvableDiscussionsCounts();
createComponent({ blocksMerge: true });
expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false);
@ -59,19 +55,15 @@ describe('DiscussionCounter component', () => {
});
describe('has resolvable discussions', () => {
const updateStore = (note = {}) => {
discussionMock.notes[0] = { ...discussionMock.notes[0], ...note };
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussionMock]);
store.dispatch('updateResolvableDiscussionsCounts');
const addNote = (note = {}) => {
const discussion = createDiscussionMock();
discussion.notes[0] = { ...discussion.notes[0], ...note };
useNotes()[types.ADD_OR_UPDATE_DISCUSSIONS]([discussion]);
useNotes().updateResolvableDiscussionsCounts();
};
afterEach(() => {
delete discussionMock.notes[0].resolvable;
delete discussionMock.notes[0].resolved;
});
it('renders', () => {
updateStore();
addNote();
createComponent({ blocksMerge: true });
expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(true);
@ -84,8 +76,8 @@ describe('DiscussionCounter component', () => {
`(
'changes background color to $color if blocksMerge is $blocksMerge',
({ blocksMerge, color }) => {
updateStore();
store.state.notes.unresolvedDiscussionsCount = 1;
addNote();
useNotes().unresolvedDiscussionsCount = 1;
createComponent({ blocksMerge });
expect(wrapper.find('[data-testid="discussions-counter-text"]').classes()).toContain(color);
@ -97,7 +89,7 @@ describe('DiscussionCounter component', () => {
${'not allResolved'} | ${false} | ${2}
${'allResolved'} | ${true} | ${1}
`('renders correctly if $title', async ({ resolved, groupLength }) => {
updateStore({ resolvable: true, resolved });
addNote({ resolvable: true, resolved });
createComponent({ blocksMerge: true });
await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
@ -106,11 +98,11 @@ describe('DiscussionCounter component', () => {
describe('resolve all with new issue link', () => {
it('has correct href prop', async () => {
updateStore({ resolvable: true });
addNote({ resolvable: true });
createComponent({ blocksMerge: true });
const resolveDiscussionsPath =
store.getters.getNoteableData.create_issue_to_resolve_discussions_path;
useNotes().getNoteableData.create_issue_to_resolve_discussions_path;
await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
const resolveAllLink = wrapper.find('[data-testid="resolve-all-with-issue-link"]');
@ -125,9 +117,9 @@ describe('DiscussionCounter component', () => {
let discussion;
const updateStoreWithExpanded = async (expanded) => {
discussion = { ...discussionMock, expanded };
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
discussion = { ...createDiscussionMock(), expanded };
useNotes()[types.ADD_OR_UPDATE_DISCUSSIONS]([discussion]);
useNotes().updateResolvableDiscussionsCounts();
createComponent({ blocksMerge: true });
await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');

View File

@ -2,8 +2,8 @@ import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { TEST_HOST } from 'helpers/test_constants';
import createEventHub from '~/helpers/event_hub_factory';
import * as urlUtility from '~/lib/utils/url_utility';
@ -18,22 +18,22 @@ import {
ASC,
DESC,
} from '~/notes/constants';
import notesModule from '~/notes/stores/modules';
import { useNotes } from '~/notes/store/legacy_notes';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { discussionFiltersMock, discussionMock } from '../mock_data';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
const DISCUSSION_PATH = `${TEST_HOST}/example`;
describe('DiscussionFilter component', () => {
let wrapper;
let store;
let pinia;
let eventHub;
let mock;
const filterDiscussion = jest.fn();
const findFilter = (filterType) =>
wrapper.find(`.gl-new-dropdown-item[data-filter-type="${filterType}"]`);
const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
@ -49,22 +49,12 @@ describe('DiscussionFilter component', () => {
},
];
const defaultStore = { ...notesModule() };
useNotes().notesData.discussionsPath = DISCUSSION_PATH;
store = new Vuex.Store({
...defaultStore,
actions: {
...defaultStore.actions,
filterDiscussion,
},
});
store.state.notesData.discussionsPath = DISCUSSION_PATH;
store.state.discussions = discussions;
useNotes().discussions = discussions;
wrapper = mount(DiscussionFilter, {
store,
pinia,
propsData: {
filters: discussionFiltersMock,
selectedValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
@ -74,6 +64,9 @@ describe('DiscussionFilter component', () => {
};
beforeEach(() => {
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs();
useNotes();
mock = new AxiosMockAdapter(axios);
// We are mocking the discussions retrieval,
@ -90,7 +83,6 @@ describe('DiscussionFilter component', () => {
describe('default', () => {
beforeEach(() => {
mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
it('has local storage sync with the correct props', () => {
@ -100,21 +92,20 @@ describe('DiscussionFilter component', () => {
it('calls setDiscussionSortDirection when update is emitted', () => {
findLocalStorageSync().vm.$emit('input', ASC);
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC });
expect(useNotes().setDiscussionSortDirection).toHaveBeenCalledWith({ direction: ASC });
});
});
describe('when asc', () => {
beforeEach(() => {
mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
describe('when the dropdown is clicked', () => {
it('calls the right actions', () => {
wrapper.find('.js-newest-first').vm.$emit('action');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
expect(useNotes().setDiscussionSortDirection).toHaveBeenCalledWith({
direction: DESC,
});
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
@ -127,15 +118,14 @@ describe('DiscussionFilter component', () => {
describe('when desc', () => {
beforeEach(() => {
mountComponent();
store.state.discussionSortOrder = DESC;
jest.spyOn(store, 'dispatch').mockImplementation();
useNotes().discussionSortOrder = DESC;
});
describe('when the dropdown item is clicked', () => {
it('calls the right actions', () => {
wrapper.find('.js-oldest-first').vm.$emit('action');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
expect(useNotes().setDiscussionSortDirection).toHaveBeenCalledWith({
direction: ASC,
});
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
@ -167,7 +157,7 @@ describe('DiscussionFilter component', () => {
});
it('disables the dropdown when discussions are loading', () => {
store.state.isLoading = true;
useNotes().isLoading = true;
expect(wrapper.findComponent(GlDisclosureDropdown).props('disabled')).toBe(true);
});
@ -183,27 +173,27 @@ describe('DiscussionFilter component', () => {
it('only updates when selected filter changes', () => {
findFilter(DISCUSSION_FILTER_TYPES.ALL).vm.$emit('action');
expect(filterDiscussion).not.toHaveBeenCalled();
expect(useNotes().filterDiscussion).not.toHaveBeenCalled();
});
it('disables timeline view if it was enabled', () => {
store.state.isTimelineEnabled = true;
useNotes().isTimelineEnabled = true;
findFilter(DISCUSSION_FILTER_TYPES.HISTORY).vm.$emit('action');
expect(store.state.isTimelineEnabled).toBe(false);
expect(useNotes().isTimelineEnabled).toBe(false);
});
it('disables commenting when "Show history only" filter is applied', () => {
findFilter(DISCUSSION_FILTER_TYPES.HISTORY).vm.$emit('action');
expect(store.state.commentsDisabled).toBe(true);
expect(useNotes().commentsDisabled).toBe(true);
});
it('enables commenting when "Show history only" filter is not applied', () => {
findFilter(DISCUSSION_FILTER_TYPES.ALL).vm.$emit('action');
expect(store.state.commentsDisabled).toBe(false);
expect(useNotes().commentsDisabled).toBe(false);
});
});
@ -262,10 +252,9 @@ describe('DiscussionFilter component', () => {
it('does not fetch discussions when there is no hash', async () => {
mountComponent();
const dispatchSpy = jest.spyOn(store, 'dispatch');
await nextTick();
expect(dispatchSpy).not.toHaveBeenCalled();
expect(useNotes().filterDiscussion).not.toHaveBeenCalled();
});
describe('selected value is not default state', () => {
@ -276,12 +265,11 @@ describe('DiscussionFilter component', () => {
});
it('fetch discussions when there is hash', async () => {
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('note_123');
const dispatchSpy = jest.spyOn(store, 'dispatch');
window.dispatchEvent(new Event('hashchange'));
await nextTick();
expect(dispatchSpy).toHaveBeenCalledWith('filterDiscussion', {
expect(useNotes().filterDiscussion).toHaveBeenCalledWith({
filter: 0,
path: 'http://test.host/example',
persistFilter: false,

View File

@ -1,15 +1,24 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import createStore from '~/notes/stores';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { notesDataMock } from '../mock_data';
Vue.use(PiniaVuePlugin);
describe('NoteSignedOutWidget component', () => {
let wrapper;
let pinia;
beforeEach(() => {
const store = createStore();
store.dispatch('setNotesData', notesDataMock);
wrapper = shallowMount(NoteSignedOutWidget, { store });
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs();
useNotes().setNotesData(notesDataMock);
wrapper = shallowMount(NoteSignedOutWidget, { pinia });
});
it('renders sign in link provided in the store', () => {

View File

@ -1,13 +1,22 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import TimelineToggle from '~/notes/components/timeline_toggle.vue';
import createStore from '~/notes/stores';
import waitForPromises from 'helpers/wait_for_promises';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
import { notesFilters } from '../mock_data';
Vue.use(PiniaVuePlugin);
describe('~/notes/components/notes_activity_header.vue', () => {
let wrapper;
let pinia;
const findTitle = () => wrapper.find('h2');
@ -19,10 +28,19 @@ describe('~/notes/components/notes_activity_header.vue', () => {
},
// why: Rendering async timeline toggle requires store
store: createStore(),
pinia,
...options,
});
};
beforeEach(() => {
pinia = createTestingPinia({
plugins: [globalAccessorPlugin],
});
useLegacyDiffs();
useNotes();
});
describe('default', () => {
beforeEach(() => {
createComponent();

View File

@ -3,13 +3,14 @@ import { createTestingPinia } from '@pinia/testing';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import VueApollo from 'vue-apollo';
import setWindowLocation from 'helpers/set_window_location_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getLocationHash } from '~/lib/utils/url_utility';
@ -20,7 +21,7 @@ import NotesApp from '~/notes/components/notes_app.vue';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
import store from '~/mr_notes/stores';
import OrderedLayout from '~/notes/components/ordered_layout.vue';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
@ -29,9 +30,12 @@ import { ISSUABLE_COMMENT_OR_REPLY, keysFor } from '~/behaviors/shortcuts/keybin
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { useNotes } from '~/notes/store/legacy_notes';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { globalAccessorPlugin, syncWithVuex } from '~/pinia/plugins';
import createMockApollo from 'helpers/mock_apollo_helper';
import noteQuery from '~/notes/graphql/note.query.graphql';
import { useBatchComments } from '~/batch_comments/store';
import * as types from '~/notes/stores/mutation_types';
import { SET_BATCH_COMMENTS_DRAFTS } from '~/batch_comments/stores/modules/batch_comments/mutation_types';
import * as mockData from '../mock_data';
jest.mock('~/behaviors/markdown/render_gfm');
@ -50,13 +54,13 @@ const propsData = {
notesFilterValue: TEST_NOTES_FILTER_VALUE,
};
Vue.use(Vuex);
Vue.use(VueApollo);
Vue.use(PiniaVuePlugin);
describe('note_app', () => {
let axiosMock;
let mountComponent;
let wrapper;
let store;
let pinia;
const initStore = (notesData = propsData.notesData) => {
@ -72,6 +76,29 @@ describe('note_app', () => {
});
};
const mountComponent = ({ props = {} } = {}) => {
initStore();
wrapper = mount(
{
components: {
NotesApp,
},
template: `<div class="js-vue-notes-event">
<notes-app ref="notesApp" v-bind="$attrs" />
</div>`,
inheritAttrs: false,
},
{
propsData: {
...propsData,
...props,
},
store,
pinia,
},
);
};
const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
const getComponentOrder = () => {
@ -84,39 +111,18 @@ describe('note_app', () => {
};
beforeEach(() => {
store.commit('reset');
$('body').attr('data-page', 'projects:merge_requests:show');
axiosMock = new AxiosMockAdapter(axios);
Vue.use(VueApollo);
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
pinia = createTestingPinia({
plugins: [globalAccessorPlugin, syncWithVuex],
stubActions: false,
});
useLegacyDiffs();
useNotes();
store = createStore();
mountComponent = ({ props = {} } = {}) => {
initStore();
return mount(
{
components: {
NotesApp,
},
template: `<div class="js-vue-notes-event">
<notes-app ref="notesApp" v-bind="$attrs" />
</div>`,
inheritAttrs: false,
},
{
propsData: {
...propsData,
...props,
},
store,
pinia,
},
);
};
useBatchComments();
});
afterEach(() => {
@ -126,7 +132,7 @@ describe('note_app', () => {
describe('render', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
@ -169,7 +175,7 @@ describe('note_app', () => {
describe('render with comments disabled', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent({
mountComponent({
// why: In this integration test, previously we manually set store.state.commentsDisabled
// This stopped working when we added `<discussion-filter>` into the component tree.
// Let's lean into the integration scope and use a prop that "disables comments".
@ -193,10 +199,9 @@ describe('note_app', () => {
describe('timeline view', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = false;
store.state.isTimelineEnabled = true;
store.commit(types.SET_TIMELINE_VIEW, true);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
@ -207,7 +212,7 @@ describe('note_app', () => {
describe('while fetching data', () => {
beforeEach(() => {
wrapper = mountComponent();
mountComponent();
});
it('renders skeleton notes', () => {
@ -226,7 +231,7 @@ describe('note_app', () => {
describe('individual note', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises().then(() => {
wrapper.find('.js-note-edit').trigger('click');
});
@ -249,7 +254,7 @@ describe('note_app', () => {
describe('discussion note', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getDiscussionNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises().then(() => {
wrapper.find('.js-note-edit').trigger('click');
});
@ -273,7 +278,7 @@ describe('note_app', () => {
describe('new note form', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
@ -287,7 +292,7 @@ describe('note_app', () => {
describe('edit form', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
@ -303,42 +308,35 @@ describe('note_app', () => {
describe('emoji awards', () => {
beforeEach(() => {
axiosMock.onAny().reply(HTTP_STATUS_OK, []);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
it('dispatches toggleAward after toggleAward event', () => {
const spy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn());
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: 'test',
noteId: 1,
},
});
const toggleAwardAction = jest.fn().mockName('toggleAward');
wrapper.vm.$store.hotUpdate({
actions: {
toggleAward: toggleAwardAction,
},
});
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
wrapper.element.dispatchEvent(toggleAwardEvent);
jest.advanceTimersByTime(2);
expect(toggleAwardAction).toHaveBeenCalledTimes(1);
const [, payload] = toggleAwardAction.mock.calls[0];
expect(payload).toEqual({
expect(spy).toHaveBeenCalledWith('toggleAward', {
awardName: 'test',
noteId: 1,
});
spy.mockRestore();
});
});
describe('mounted', () => {
beforeEach(() => {
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
mountComponent();
return waitForPromises();
});
@ -354,14 +352,14 @@ describe('note_app', () => {
describe('when sort direction is desc', () => {
beforeEach(() => {
store = createStore();
store.state.discussionSortOrder = constants.DESC;
store.state.isLoading = true;
store.state.discussions = [mockData.discussionMock];
store.commit(types.SET_DISCUSSIONS_SORT, { direction: constants.DESC });
store.commit(types.SET_NOTES_LOADING_STATE, true);
store.commit(types.ADD_NEW_NOTE, { discussion: mockData.discussionMock });
wrapper = shallowMount(NotesApp, {
propsData,
store,
pinia,
stubs: {
'ordered-layout': OrderedLayout,
},
@ -379,13 +377,13 @@ describe('note_app', () => {
describe('when sort direction is asc', () => {
beforeEach(() => {
store = createStore();
store.state.isLoading = true;
store.state.discussions = [mockData.discussionMock];
store.commit(types.SET_NOTES_LOADING_STATE, true);
store.commit(types.ADD_NEW_NOTE, { discussion: mockData.discussionMock });
wrapper = shallowMount(NotesApp, {
propsData,
store,
pinia,
stubs: {
'ordered-layout': OrderedLayout,
},
@ -411,13 +409,13 @@ describe('note_app', () => {
.fn()
.mockResolvedValue(mockData.singleNoteResponseFactory({ urlHash, authorId }));
store = createStore();
store.state.isLoading = true;
store.state.targetNoteHash = urlHash;
store.commit(types.SET_NOTES_LOADING_STATE, true);
store.commit(types.SET_TARGET_NOTE_HASH, urlHash);
wrapper = shallowMount(NotesApp, {
propsData,
store,
pinia,
apolloProvider: createMockApollo([[noteQuery, noteQueryHandler]]),
stubs: {
'ordered-layout': OrderedLayout,
@ -460,23 +458,26 @@ describe('note_app', () => {
describe('when multiple draft types are present', () => {
beforeEach(() => {
store = createStore();
store.registerModule('batchComments', batchComments());
store.state.batchComments.drafts = [
store.commit(types.SET_NOTES_LOADING_STATE, true);
store.commit(`batchComments/${SET_BATCH_COMMENTS_DRAFTS}`, [
mockData.draftDiffDiscussion,
mockData.draftReply,
...mockData.draftComments,
];
store.state.isLoading = false;
]);
wrapper = shallowMount(NotesApp, {
propsData,
store,
pinia,
stubs: {
OrderedLayout,
},
});
});
afterEach(() => {
store.commit('batchComments/reset');
});
it('correctly finds only draft comments', () => {
const drafts = wrapper.findAllComponents(DraftNote).wrappers;
@ -489,9 +490,8 @@ describe('note_app', () => {
describe('fetching discussions', () => {
describe('when note anchor is not present', () => {
it('does not include extra query params', async () => {
store = createStore();
initStore();
wrapper = shallowMount(NotesApp, { propsData, store });
wrapper = shallowMount(NotesApp, { propsData, store, pinia });
await waitForPromises();
expect(axiosMock.history.get[0].params).toEqual({ per_page: 20 });
@ -506,7 +506,8 @@ describe('note_app', () => {
});
return shallowMount(NotesApp, {
propsData,
store: createStore(),
store,
pinia,
});
};
@ -548,7 +549,7 @@ describe('note_app', () => {
window.mrTabs = { eventHub: notesEventHub };
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
trackingSpy = mockTracking(undefined, window.document, jest.spyOn);
wrapper = mountComponent();
mountComponent();
});
describe('when adding a new comment to an existing review', () => {
@ -588,7 +589,7 @@ describe('note_app', () => {
it('sends quote to main reply editor', async () => {
jest.spyOn(CopyAsGFM, 'selectionToGfm').mockReturnValueOnce('foo');
wrapper = mountComponent();
mountComponent();
const replySpy = jest.spyOn(wrapper.findComponent(CommentForm).vm, 'append');
const target = wrapper.element.querySelector('p');
stubSelection(target);
@ -600,7 +601,7 @@ describe('note_app', () => {
it('sends quote to discussion reply editor', async () => {
jest.spyOn(CopyAsGFM, 'selectionToGfm').mockReturnValueOnce('foo');
axiosMock.onAny().reply(mockData.getDiscussionNoteResponse);
wrapper = mountComponent();
mountComponent();
await waitForPromises();
const replySpy = jest.spyOn(wrapper.findComponent(NoteableDiscussion).vm, 'showReplyForm');
const target = wrapper.element.querySelector('.js-noteable-discussion p');
@ -613,7 +614,6 @@ describe('note_app', () => {
describe('noteableType computed property', () => {
const createComponent = (noteableType, type) => {
store = createStore();
return shallowMount(NotesApp, {
propsData: {
...propsData,
@ -624,6 +624,7 @@ describe('note_app', () => {
},
},
store,
pinia,
});
};

View File

@ -1,65 +1,66 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import TimelineToggle, {
timelineEnabledTooltip,
timelineDisabledTooltip,
} from '~/notes/components/timeline_toggle.vue';
import { ASC, DESC } from '~/notes/constants';
import createStore from '~/notes/stores';
import { trackToggleTimelineView } from '~/notes/utils';
import Tracking from '~/tracking';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { useNotes } from '~/notes/store/legacy_notes';
Vue.use(Vuex);
Vue.use(PiniaVuePlugin);
describe('Timeline toggle', () => {
let wrapper;
let store;
let pinia;
const mockEvent = { currentTarget: { blur: jest.fn() } };
const createComponent = () => {
jest.spyOn(store, 'dispatch').mockImplementation();
jest.spyOn(Tracking, 'event').mockImplementation();
wrapper = mount(TimelineToggle, {
store,
pinia,
});
};
const findGlButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
store = createStore();
createComponent();
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs();
useNotes();
});
afterEach(() => {
store.dispatch.mockReset();
mockEvent.currentTarget.blur.mockReset();
Tracking.event.mockReset();
});
describe('ON state', () => {
it('should update timeline flag in the store', () => {
store.state.isTimelineEnabled = false;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setTimelineView', true);
expect(useNotes().setTimelineView).toHaveBeenCalledWith(true);
});
it('should set sort direction to DESC if not set', () => {
store.state.isTimelineEnabled = true;
store.state.sortDirection = ASC;
useNotes().discussionSortOrder = ASC;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
expect(useNotes().setDiscussionSortDirection).toHaveBeenCalledWith({
direction: DESC,
persist: false,
});
});
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = true;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
await nextTick();
expect(findGlButton().attributes('title')).toBe(timelineEnabledTooltip);
@ -68,7 +69,7 @@ describe('Timeline toggle', () => {
});
it('should track Snowplow event', async () => {
store.state.isTimelineEnabled = true;
createComponent();
await nextTick();
findGlButton().trigger('click');
@ -79,20 +80,24 @@ describe('Timeline toggle', () => {
});
describe('OFF state', () => {
beforeEach(() => {
useNotes().isTimelineEnabled = true;
});
it('should update timeline flag in the store', () => {
store.state.isTimelineEnabled = true;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setTimelineView', false);
expect(useNotes().setTimelineView).toHaveBeenCalledWith(false);
});
it('should NOT update sort direction', () => {
store.state.isTimelineEnabled = false;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).not.toHaveBeenCalledWith();
expect(useNotes().setDiscussionSortDirection).not.toHaveBeenCalled();
});
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = false;
createComponent();
findGlButton().vm.$emit('click', mockEvent);
await nextTick();
expect(findGlButton().attributes('title')).toBe(timelineDisabledTooltip);
@ -100,9 +105,8 @@ describe('Timeline toggle', () => {
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
});
it('should track Snowplow event', async () => {
store.state.isTimelineEnabled = false;
await nextTick();
it('should track Snowplow event', () => {
createComponent();
findGlButton().trigger('click');

View File

@ -178,7 +178,7 @@ export const note = {
path: '/gitlab-org/gitlab-foss/notes/546',
};
export const discussionMock = {
export const createDiscussionMock = () => ({
id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
expanded: true,
@ -323,7 +323,9 @@ export const discussionMock = {
resolvable: true,
active: true,
confidential: false,
};
});
export const discussionMock = createDiscussionMock();
export const loggedOutnoteableData = {
id: '98',

View File

@ -60,13 +60,15 @@ describe('Pinia plugins', () => {
});
};
const createPiniaStore = () => {
const createSyncWithConfig = () => ({
store: vuexStore,
name,
namespaced,
});
const createPiniaStore = (syncWith = createSyncWithConfig()) => {
usePiniaStore = defineStore('exampleStore', {
syncWith: {
store: vuexStore,
name,
namespaced,
},
syncWith,
state() {
return createState();
},
@ -113,6 +115,17 @@ describe('Pinia plugins', () => {
setActivePinia(createPinia().use(syncWithVuex));
},
],
[
'with a non namespaced config override',
() => {
name = 'myStore';
namespaced = false;
createVuexStoreWithModule();
createPiniaStore(undefined);
setActivePinia(createPinia().use(syncWithVuex));
usePiniaStore().syncWith(createSyncWithConfig());
},
],
])('%s', (caseName, setupFn) => {
beforeEach(() => {
setupFn();

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ViteHelper, feature_category: :tooling do
describe '#vite_page_entrypoint_path' do
describe '#vite_page_entrypoint_paths' do
using RSpec::Parameterized::TableSyntax
where(:path, :action, :result) do
@ -93,36 +93,4 @@ RSpec.describe ViteHelper, feature_category: :tooling do
end
end
end
describe '#vite_origin' do
before do
allow(ViteRuby).to receive_message_chain(:config, :origin).and_return('origin')
end
it { expect(helper.vite_origin).to eq('origin') }
end
describe '#vite_hmr_ws_origin' do
before do
allow(ViteRuby).to receive_message_chain(:config, :host_with_port).and_return('host')
allow(ViteRuby).to receive_message_chain(:config, :https).and_return(https)
end
context 'with https' do
let(:https) { true }
it 'returns wss origin' do
expect(helper.vite_hmr_ws_origin).to eq('wss://host')
end
end
context 'without https' do
let(:https) { false }
it 'returns ws origin' do
allow(ViteRuby).to receive_message_chain(:config, :https).and_return(false)
expect(helper.vite_hmr_ws_origin).to eq('ws://host')
end
end
end
end

View File

@ -39,8 +39,8 @@ RSpec.describe Gitlab::Ci::Build::Releaser, feature_category: :continuous_integr
release_cli_command = 'release-cli create --name "Release $CI_COMMIT_SHA" --description "Created using the release-cli $EXTRA_DESCRIPTION" --tag-name "release-$CI_COMMIT_SHA" --tag-message "Annotated tag message" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3"'
result_for_release_cli_without_catalog_publish = "#{release_cli_command} #{release_cli_assets_links}"
glab_create_unix = 'glab -R $CI_PROJECT_PATH release create'
glab_create_windows = 'glab -R $env:CI_PROJECT_PATH release create'
glab_create_unix = 'glab release create -R $CI_PROJECT_PATH'
glab_create_windows = 'glab release create -R $env:CI_PROJECT_PATH'
glab_command = "\"release-$CI_COMMIT_SHA\" #{glab_assets_links} --milestone \"m1,m2,m3\" --name \"Release $CI_COMMIT_SHA\" --experimental-notes-text-or-file \"Created using the release-cli $EXTRA_DESCRIPTION\" --ref \"$CI_COMMIT_SHA\" --tag-message \"Annotated tag message\" --released-at \"2020-07-15T08:00:00Z\" --no-update --no-close-milestone"
warning_message = "Warning: release-cli will not be supported after 18.0. Please use glab version >= 1.53.0. Troubleshooting: http://localhost/help/user/project/releases/_index.md#gitlab-cli-version-requirement"
@ -206,14 +206,14 @@ RSpec.describe Gitlab::Ci::Build::Releaser, feature_category: :continuous_integr
links = { links: [{ name: 'asset1', url: 'https://example.com/assets/1', link_type: 'other', filepath: '/pretty/asset/1' }] }
where(:node_name, :node_value, :result) do
:name | 'Release $CI_COMMIT_SHA' | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --name "Release $CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA"'
:description | 'Release-cli $EXTRA_DESCRIPTION' | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --experimental-notes-text-or-file "Release-cli $EXTRA_DESCRIPTION" --ref "$CI_COMMIT_SHA"'
:tag_name | 'release-$CI_COMMIT_SHA' | 'glab -R $CI_PROJECT_PATH release create "release-$CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA"'
:tag_message | 'Annotated tag message' | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA" --tag-message "Annotated tag message"'
:ref | '$CI_COMMIT_SHA' | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA"'
:milestones | %w[m1 m2 m3] | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --milestone "m1,m2,m3" --ref "$CI_COMMIT_SHA"'
:released_at | '2020-07-15T08:00:00Z' | 'glab -R $CI_PROJECT_PATH release create "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z"'
:assets | links | "glab -R $CI_PROJECT_PATH release create \"$CI_COMMIT_TAG\" --assets-links #{links[:links].to_json.to_json} --ref \"$CI_COMMIT_SHA\""
:name | 'Release $CI_COMMIT_SHA' | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --name "Release $CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA"'
:description | 'Release-cli $EXTRA_DESCRIPTION' | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --experimental-notes-text-or-file "Release-cli $EXTRA_DESCRIPTION" --ref "$CI_COMMIT_SHA"'
:tag_name | 'release-$CI_COMMIT_SHA' | 'glab release create -R $CI_PROJECT_PATH "release-$CI_COMMIT_SHA" --ref "$CI_COMMIT_SHA"'
:tag_message | 'Annotated tag message' | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA" --tag-message "Annotated tag message"'
:ref | '$CI_COMMIT_SHA' | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA"'
:milestones | %w[m1 m2 m3] | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --milestone "m1,m2,m3" --ref "$CI_COMMIT_SHA"'
:released_at | '2020-07-15T08:00:00Z' | 'glab release create -R $CI_PROJECT_PATH "$CI_COMMIT_TAG" --ref "$CI_COMMIT_SHA" --released-at "2020-07-15T08:00:00Z"'
:assets | links | "glab release create -R $CI_PROJECT_PATH \"$CI_COMMIT_TAG\" --assets-links #{links[:links].to_json.to_json} --ref \"$CI_COMMIT_SHA\""
end
with_them do

View File

@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Build::Step, feature_category: :continuous_integratio
let(:job) { create(:ci_build, :release_options) }
it 'returns glab command line' do
expect(subject.script).to match_array([a_string_including("glab -R $CI_PROJECT_PATH release create")])
expect(subject.script).to match_array([a_string_including("glab release create -R $CI_PROJECT_PATH")])
end
context 'when the FF ci_glab_for_release is disabled' do
@ -92,7 +92,7 @@ RSpec.describe Gitlab::Ci::Build::Step, feature_category: :continuous_integratio
let(:job) { create(:ci_build, :release_options, pipeline: pipeline) }
it 'returns glab scripts with catalog publish' do
expect(subject.script).to match_array([a_string_including("glab -R $CI_PROJECT_PATH release create")])
expect(subject.script).to match_array([a_string_including("glab release create -R $CI_PROJECT_PATH")])
expect(subject.script).to match_array([a_string_including("--publish-to-catalog")])
end

View File

@ -6,6 +6,8 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
let(:policy) { ActionDispatch::ContentSecurityPolicy.new }
let(:lfs_enabled) { false }
let(:proxy_download) { false }
let(:host) { "gdk.test" }
let(:port) { 3443 }
let(:csp_config) do
{
@ -43,6 +45,11 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
end
before do
ViteRuby.configure(
host: host,
port: port,
https: true
)
stub_lfs_setting(enabled: lfs_enabled)
allow(LfsObjectUploader)
.to receive(:object_store_options)
@ -110,7 +117,78 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
describe 'the worker-src directive' do
it 'can be loaded from local origins' do
expect(worker_src).to eq("'self' http://localhost/assets/ blob: data:")
expect(worker_src).to eq("'self' http://localhost/assets/ blob: data: https://gdk.test:3443/vite-dev/")
end
end
describe 'Vite dev server' do
using RSpec::Parameterized::TableSyntax
where(:https, :env) do
[
[true, 'development'],
[true, 'test'],
[false, 'development'],
[false, 'test']
]
end
with_them do
def protocol
https ? 'https' : 'http'
end
def ws_protocol
https ? 'wss' : 'ws'
end
def origin
"#{protocol}://#{ViteRuby.config.host_with_port}"
end
def dev_server_path
"#{origin}/vite-dev"
end
def dev_server_socket_path
"#{ws_protocol}://#{ViteRuby.config.host_with_port}/vite-dev"
end
before do
ViteRuby.configure(
host: host,
port: port,
https: https
)
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'does not add directives' do
expect(connect_src).not_to include(dev_server_path)
expect(connect_src).not_to include(dev_server_socket_path)
expect(worker_src).not_to include(dev_server_path)
expect(style_src).not_to include(dev_server_path)
expect(font_src).not_to include(dev_server_path)
end
end
context 'when in non-production' do
before do
stub_rails_env(env)
end
it 'adds directives' do
expect(connect_src).to include(dev_server_path)
expect(connect_src).to include(dev_server_socket_path)
expect(worker_src).to include(dev_server_path)
expect(style_src).to include(dev_server_path)
expect(font_src).to include(dev_server_path)
end
end
end
end
@ -183,28 +261,28 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
describe 'Websocket connections' do
it 'with insecure domain' do
stub_config_setting(host: 'example.com', https: false)
expect(connect_src).to eq("'self' ws://example.com")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://example.com")
end
it 'with secure domain' do
stub_config_setting(host: 'example.com', https: true)
expect(connect_src).to eq("'self' wss://example.com")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com")
end
it 'with custom port' do
stub_config_setting(host: 'example.com', port: '1234')
expect(connect_src).to eq("'self' ws://example.com:1234")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://example.com:1234")
end
it 'with custom port and secure domain' do
stub_config_setting(host: 'example.com', https: true, port: '1234')
expect(connect_src).to eq("'self' wss://example.com:1234")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com:1234")
end
it 'when port is included in HTTP_PORTS' do
described_class::HTTP_PORTS.each do |port|
stub_config_setting(host: 'example.com', https: true, port: port)
expect(connect_src).to eq("'self' wss://example.com")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ wss://example.com")
end
end
end
@ -323,9 +401,9 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
it 'does not include CDN host in CSP' do
expect(script_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src)
expect(style_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src)
expect(font_src).to eq("'self'")
expect(worker_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.worker_src)
expect(style_src).to eq("#{::Gitlab::ContentSecurityPolicy::Directives.style_src} https://gdk.test:3443/vite-dev/")
expect(font_src).to eq("'self' https://gdk.test:3443/vite-dev/")
expect(worker_src).to eq("#{::Gitlab::ContentSecurityPolicy::Directives.worker_src} https://gdk.test:3443/vite-dev/")
expect(frame_src).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src)
end
end
@ -359,7 +437,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
end
it 'adds new sentry path to CSP' do
expect(connect_src).to eq("'self' ws://gitlab.example.com dummy://sentry.example.com")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://gitlab.example.com dummy://sentry.example.com")
end
end
@ -373,7 +451,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
end
it 'config is backwards compatible, does not add sentry path to CSP' do
expect(connect_src).to eq("'self' ws://gitlab.example.com")
expect(connect_src).to eq("'self' wss://gdk.test:3443/vite-dev/ https://gdk.test:3443/vite-dev/ ws://gitlab.example.com")
end
end
end

View File

@ -16,7 +16,8 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::DeployMenu, feature_category
expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem])
expect(items.map(&:item_id)).to eq([
:packages_registry,
:container_registry
:container_registry,
:virtual_registry
])
end
end

View File

@ -442,7 +442,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
},
{
"name" => "release",
"script" => [a_string_including("glab -R $CI_PROJECT_PATH release create")],
"script" => [a_string_including("glab release create -R $CI_PROJECT_PATH")],
"timeout" => 3600,
"when" => "on_success",
"allow_failure" => false

View File

@ -282,39 +282,4 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared
end
end
end
context 'when configuring vite' do
let(:vite_hmr_ws_origin) { 'ws://gitlab.example.com:3808' }
let(:vite_origin) { 'http://gitlab.example.com:3808' }
before do
# rubocop:disable RSpec/AnyInstanceOf -- Doesn't work with allow_next_instance_of
allow_any_instance_of(ViteHelper)
.to receive_messages(
vite_enabled?: vite_enabled,
vite_hmr_ws_origin: vite_hmr_ws_origin,
vite_origin: vite_origin,
universal_path_to_stylesheet: '')
# rubocop:enable RSpec/AnyInstanceOf
end
context 'when vite enabled during development' do
let(:vite_enabled) { true }
it 'adds vite csp' do
get root_path
expect(response.headers['Content-Security-Policy']).to include("#{vite_hmr_ws_origin}/vite-dev/")
expect(response.headers['Content-Security-Policy']).to include("#{vite_origin}/vite-dev/")
end
end
context 'when vite is disabled' do
let(:vite_enabled) { false }
it "doesn't add vite csp" do
get root_path
expect(response.headers['Content-Security-Policy']).not_to include('/vite-dev/')
end
end
end
end

View File

@ -71,6 +71,14 @@ module NavbarStructureHelper
)
end
def insert_virtual_registry_nav
insert_after_sub_nav_item(
_('Package registry'),
within: _('Deploy'),
new_sub_nav_item_name: _('Virtual registry')
)
end
def insert_google_artifact_registry_nav
insert_after_sub_nav_item(
_('Container registry'),

292
yarn.lock
View File

@ -3223,181 +3223,181 @@
dom-accessibility-api "^0.5.1"
pretty-format "^26.4.2"
"@tiptap/core@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.13.0.tgz#5ad9a1f3980cee1379493b3c0c94234a2a716808"
integrity sha512-VDwlf5+DznkrAT+vlggl9t/mPVeo3ayi4irXgLPkDfHAY8adVIx+RCek6GBChclJ8q2Iy0HZwpIYs/8L7tadqA==
"@tiptap/core@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.14.0.tgz#9a0ffd500cc720194916475506292006d2cb69c3"
integrity sha512-MBSMzGYRFlwYCocvx3dU7zpCBSDQ0qWByNtStaEzuBUgzCJ6wn2DP/xG0cMcLmE3Ia0VLM4nwbLOAAvBXOtylA==
"@tiptap/extension-blockquote@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.13.0.tgz#1c5f12f88298a43e03718f0fd81485e4589fe548"
integrity sha512-DQjd8AUG9TE+ReFp05T9N5Z+o4dw88j+O6xgPMjs1T1u9yHUMxwjvidPRi5QqCqPiwDJcqm3bjYxebPPMHbi9w==
"@tiptap/extension-blockquote@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.14.0.tgz#e5a660f0afc2b372da4c21545517e74d2c770b68"
integrity sha512-AwqPP0jLYNioKxakiVw0vlfH/ceGFbV+SGoqBbPSGFPRdSbHhxHDNBlTtiThmT3N2PiVwXAD9xislJV+WY4GUA==
"@tiptap/extension-bold@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.13.0.tgz#ed552467b7e4c598c94e4be234644d4a6e9f1f6f"
integrity sha512-q/Kqo1HXas+dUevP/Qice+nbxXue8ZpmYBniw9zt/JHbgwH1b6Rw7lIjLxYerdaPWj305h9ZHxLqmzDOEcQRPw==
"@tiptap/extension-bold@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.14.0.tgz#53655a81fd11304a83cc654fc4071e519170674d"
integrity sha512-8DWwelH55H8KtLECSIv0wh8x/F/6lpagV/pMvT+Azujad0oqK+1iAPKU/kLgjXbFSkisrpV6KSwQts5neCtfRQ==
"@tiptap/extension-bubble-menu@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.13.0.tgz#4ad8e4ccc45c8e7a90e7f04e099f2c23aa214a99"
integrity sha512-y2PRg7YT8Km1e4+xEvXcKTPfEu/i44eKNjbsKojgs70kuONdhFmhWIXCeGEVAwPH8ZPH+JPam5kcW2vsihoayg==
"@tiptap/extension-bubble-menu@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.14.0.tgz#d51752558323c89ed45ca0118265b9a9e4226166"
integrity sha512-sN15n0RjPh+2Asvxs7l47hVEvX6c0aPempU8QQWcPUlHoGf1D/XkyHXy6GWVPSxZ5Rj5uAwgKvhHsG/FJ/YGKQ==
dependencies:
tippy.js "^6.3.7"
"@tiptap/extension-bullet-list@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.13.0.tgz#6392a0970ab39db9dffdd6d6b58f3a5689ed8ee2"
integrity sha512-7XMNtRFCC+Co7FrwyU5gUC+4i3k83kQb1KJcYjq6Deud/2DoXNJeRcacqXr+mNePm+W71xZJ7W7ePWLlHrfHPw==
"@tiptap/extension-bullet-list@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.14.0.tgz#9be7a09f792320a1baf33daf3dd88d149b9cd2c2"
integrity sha512-SWnL4bP8Mm/mWN42AMQNoqYE0V6LgSBTVsHwwAki2wIUQdr9HyoAnohvHy3IME56NMwoyZyo+Mzl45wOqUxziA==
"@tiptap/extension-code-block-lowlight@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.13.0.tgz#088ed24a1e3ecc93c33dc698b506851ed517b97e"
integrity sha512-8iIbOLUpkhYBgovxauDVwr52IRdMpE4hFKt1ktSvnC8jpT+I01vaWojew8qU3A/5ZVNe7EkBEXHG0B0gvzCznA==
"@tiptap/extension-code-block-lowlight@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.14.0.tgz#2ccf83471012d8b27b6002154c0b53802061858f"
integrity sha512-jGcVOkcThwzLdXf56zYkmB0tcB8Xy3S+ImS3kDzaccdem6qCG05JeE33K8bfPqh99OU1QqO9XdHNO9x77A2jug==
"@tiptap/extension-code-block@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.13.0.tgz#8c7cf37baefdca16896584cd7ba951673e480488"
integrity sha512-HGcWmwKx4D53HY4XO0ve6lNHirpWdd91AVGVPuIkyG0wSvAzBgXy4TIgyUrHxGRE4qvNAe685RCYIA/VMBJAyg==
"@tiptap/extension-code-block@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.14.0.tgz#7a1f85388bed7ab3f5e98f9121c7c9bebf7b7583"
integrity sha512-LRYYZeh8U2XgfTsJ4houB9s9cVRt7PRfVa4MaCeOYKfowVOKQh67yV5oom8Azk9XrMPkPxDmMmdPAEPxeVYFvw==
"@tiptap/extension-code@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.13.0.tgz#67665a018703322e9ad137cb54060001e00dc48f"
integrity sha512-TrMhcRKsxmpZyHEstMdXAjwe+3bTqsSdiLsotzV7LwO8eCyeN42xfm2yzz9h/SJGUsO1X70YhoLH8liBNif97A==
"@tiptap/extension-code@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.14.0.tgz#cbbc58f073478e70b2217e50fb7aad23b7ace88c"
integrity sha512-kyo02mnzqgwXayMcyRA/fHQgb+nMmQQpIt1irZwjtEoFZshA7NnY/6b5SJmRcxQ4/X4r2Y2Ha2sWmOcEkLmt4A==
"@tiptap/extension-document@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.13.0.tgz#cde03f03229dcbfc4a322a77e2c282640271f5c6"
integrity sha512-jZ9NRUtJ2g67XTFgfn/6hMz+N4XJ+kG0/4GeLU2B7ZMF3UdcMWTvgyLxUNXvuLgMbCATVFTDTZiYTXgB/nPAVw==
"@tiptap/extension-document@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.14.0.tgz#9f631caa8b9a3d5cc448ffaf32e3a22326d87ef1"
integrity sha512-qwEgpPIJ3AgXdEtRTr88hODbXRdt14VAwLj27PTSqexB5V7Ra1Jy7iQDhqRwBCoUomVywBsWYxkSuDisSRG+9w==
"@tiptap/extension-dropcursor@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.13.0.tgz#cdb09cb4d59ffdb574eb56deb37b1f2aca421fb0"
integrity sha512-nx2PjBiuOb3vBUDKGSzHtfKcWJstaHGr4dx3C+TsJ3Z8qHl4tB9Ud+c7Uaz6/DptK52Q3nn2WAK2mp/njLvXuA==
"@tiptap/extension-dropcursor@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.14.0.tgz#8036c782790fc96c17daa1def0e3f03279745058"
integrity sha512-FIh5cdPuoPKvZ0GqSKhzMZGixm05ac3hSgqhMNCBZmXX459qBUI9CvDl/uzSnY9koBDeLVV3HYMthWQQLSXl9A==
"@tiptap/extension-floating-menu@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.13.0.tgz#f9a3f785b1750b5814296a7c6d085d56d938184f"
integrity sha512-gF14Nu61QUWWJDxOxzB679uK0W/rWcU7FTn1ll2zGt3NW2P2HheLo6qL1U5Wwxo3YwXloM8KLofdWi6vMN5RQQ==
"@tiptap/extension-floating-menu@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.14.0.tgz#8fc400b0d47c2898552ef09bb0952bf3e6142ddb"
integrity sha512-Khx7M7RfZlD1/T/PUlpJmao6FtEBa2L6td2hhaW1USflwGJGk0U/ud4UEqh+aZoJZrkot/EMhEvzmORF3nq+xw==
dependencies:
tippy.js "^6.3.7"
"@tiptap/extension-gapcursor@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.13.0.tgz#9a8ee0098e8fe111b26a9fdabff02b631deb5bf3"
integrity sha512-rOYVPa+mBksgGn+o9KGzKXhPNabSUVxvOB5BBwFhc0pTD+icoNk5awcUNfKx1VcBqXE3ghUvR/EEro9PvwTtdg==
"@tiptap/extension-gapcursor@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.14.0.tgz#cc2a9296df36879816b8cb4121f58b0a0b3c415a"
integrity sha512-as+SqC39FRshw4Fm1XVlrdSXveiusf5xiC4nuefLmXsUxO7Yx67x8jS0/VQbxWTLHZ6R1YEW8prLtnxGmVLCAQ==
"@tiptap/extension-hard-break@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.13.0.tgz#087b3cda05080219444a0867e064d69643afc3ed"
integrity sha512-1n83Uq7r/x/vJecj46ZO/a+MrAVDYFofz2J1V+N42EindNXaf/c2I9dIR8yxIXH8NT211gd1MckRM43eSSXU0w==
"@tiptap/extension-hard-break@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.14.0.tgz#ff1ac6be4c09b85fe01d1a3782360d994b570abc"
integrity sha512-A8c8n8881iBq3AusNqibh6Hloybr+FgYdg4Lg4jNxbbEaL0WhyLFge1bWlGVpbHXFqdv5YldMUAu6Rop3FhNvw==
"@tiptap/extension-heading@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.13.0.tgz#5e75f555b207de50fa1108df27df5c761f1c58fd"
integrity sha512-85G/DrrywpMQtZYwZJFwR+2ocRdvUM3FMLO4JVIZA7DyFsMD59lAixSSdBC2yZyr2Y8uzMW/UYyJdE+gpO6k7A==
"@tiptap/extension-heading@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.14.0.tgz#c5a9dc761712e9c87073ba8446548cbe4d403360"
integrity sha512-vM//6G3Ox3mxPv9eilhrDqylELCc8kEP1aQ4xUuOw7vCidjNtGggOa1ERnnpV2dCa2A9E8y4FHtN4Xh29stXQg==
"@tiptap/extension-highlight@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.13.0.tgz#f2aac9b7efa6d3f414eddb232f46c6bb3a209ceb"
integrity sha512-9ElpTg7VV+hcMXTTUPcq+zaNI/sNTPVNMS9GHqEGnOYABndnlsvcs1VRgAbWuZO6Hor2kKOXOOyrjY987RpyLA==
"@tiptap/extension-highlight@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.14.0.tgz#7a40ce7113d369e38d4dfa2d625deda386164b37"
integrity sha512-21eouZEuCBFrpGeefnnU9yJ1SH32L9gSlT9MOJXBSXCX5HFskNLdN8Q4cQSyRXSt6r5kEz1GG5a4I805/U2TMQ==
"@tiptap/extension-history@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.13.0.tgz#3b2089eee52694900532dfe24f9b867b3cf176b9"
integrity sha512-0gaCTHYrsECs8ZeG3oX3fyTyvKJHuGUuCnbEBfvGVxlYdhDVyuloe+G3s3UFeVVQJKO/P5OcxvUuAQ06s29tXw==
"@tiptap/extension-history@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.14.0.tgz#e7e29427c22845567c0fece8dffc32d2beeecfff"
integrity sha512-/qnOHQFCEPfkb3caykqd+sqzEC2gx30EQB/mM7+5kIG7CQy7XXaGjFAEaqzE1xJ783Q2E7GVk4JxWM+3NhYSLw==
"@tiptap/extension-horizontal-rule@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.13.0.tgz#9d9aebe75e5d399369d7bf082fefb7f637a7efaf"
integrity sha512-8gqYh2f7OAgEckIEoqsj98omJSPUOOWUvJRL50q70AUceMvPtVngWGKfITlDy4eYvRj5WXrNRSYrQrCU/8S8sg==
"@tiptap/extension-horizontal-rule@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.14.0.tgz#f777c8d9eb945ef50d865a37784fd0dc6c982674"
integrity sha512-OrKWgHOhmJtVHjPYaEJetNLiNEvrI85lTrGxzeQa+a8ACb93h4svyHe9J+LHs5pKkXDQFcpYEXJntu0LVLLiDw==
"@tiptap/extension-image@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.13.0.tgz#1e1b2fc47f35138c8462354f6724be96f3a67db0"
integrity sha512-FT/Lpr81fFMfeT4Juk8c29XxUVBEgi7CR1E/w4Z5rtcQxVUNbS00QMFktuHL3Fbh/8PaUcOZtFCkZkrVP2iQmw==
"@tiptap/extension-image@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.14.0.tgz#151c96c302b0f30dd4fe90fb5ec637531264e40e"
integrity sha512-pYCUzZBgsxIvVGTzuW03cPz6PIrAo26xpoxqq4W090uMVoK0SgY5W5y0IqCdw4QyLkJ2/oNSFNc2EP9jVi1CcQ==
"@tiptap/extension-italic@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.13.0.tgz#143591ed04b5fba52bff12a80e6bf557afe9bfaf"
integrity sha512-eI5TtUgfvwIkj576pWWgHQjRItEe7IWG+t/ZyRDMo5eTxz5b6lqsJydexmJbdL/u25kMgtwd39v7QEu0OiPcVg==
"@tiptap/extension-italic@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.14.0.tgz#b250dc6d95ba15e73f37230f07bacb0946fc042d"
integrity sha512-yEw2S+smoVR8DMYQMAWckVW2Sstf7z5+GBZ8zm8NMGhMKb1JFCPZUv5KTTIPnq7ZrKuuZHvjN9+Ef1dRYD8T2A==
"@tiptap/extension-link@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.13.0.tgz#a1bf3ada603a8a7adf9811cd376a5f308759818e"
integrity sha512-kYCuf23Do1IWVmqDuD7V9K1nW0pEj1hUEtkGrZoGMYTcph9csYUT6gpwaflOYe66A7fVdz36gSKZjO+7XznWNg==
"@tiptap/extension-link@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.14.0.tgz#c858eb67eb9c651b804fae5d324f30431884e75f"
integrity sha512-fsqW7eRD2xoD6xy7eFrNPAdIuZ3eicA4jKC45Vcft/Xky0DJoIehlVBLxsPbfmv3f27EBrtPkg5+msLXkLyzJA==
dependencies:
linkifyjs "^4.2.0"
"@tiptap/extension-list-item@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.13.0.tgz#77dea61478c1ce15db9801b3895c20c78df64267"
integrity sha512-EDq4Xm/dPjvEhdnaVJlsjV/qjdNtCCFAgO7eBzHvXP14b8feVmqJdDGs6sEZznAGxDe9gjRNsDj9EABEkQK8Dg==
"@tiptap/extension-list-item@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.14.0.tgz#da5c9d1747ece9dcbf0abdb0f85ad18857f76d35"
integrity sha512-t1jXDPEd82sC6vZVE/12/CB52uuiydCIcRfwdh21xNgBMckToKO9S0K6XEp4ROtrKQdlIH2JDVPfpUBvVrYN8Q==
"@tiptap/extension-ordered-list@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.13.0.tgz#4d4c80a62ec7b32145f9fa16462ae7b1a4e73179"
integrity sha512-krHS6lRf3R+m2eNpgHIfqI/A4CcbNoAWyQFH7HXkSuh0bgO7Aq1zi39clVcYfnI7eFGy07Lx8dQZ2kTLQ/n4Lg==
"@tiptap/extension-ordered-list@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.14.0.tgz#53e8fae14f40c19b0c72d1afa8a772285fe021dd"
integrity sha512-QUZcyuW9AKvSfpFHcGmbyRCqxcpY0VNf0xipEtogxbA+JDDw3ZSPqU1dUgz9wk00RahPTwNDdY5aVjdQ5N4N9Q==
"@tiptap/extension-paragraph@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.13.0.tgz#dc4aba50dbd5f691461fa223b47883e3b72c4425"
integrity sha512-icWUqD6j3VCIW8RTFoknBDSOwWruraJwSwfUKadEoplRSIz81J75cmoExscO8rHMD8juhIrKGbs5WD5WJajrpQ==
"@tiptap/extension-paragraph@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.14.0.tgz#9116a961b618c27974bb95cbfc40f7630c9e20e7"
integrity sha512-bsQesVpgvDS2e+wr2fp59QO7rWRp2FqcJvBafwXS3Br9U5Mx3eFYryx4wC7cUnhlhUwX5pmaoA7zISgV9dZDgg==
"@tiptap/extension-strike@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.13.0.tgz#c95a90cb0dddf36865fb6aa6e98f03489c719322"
integrity sha512-1j7ho8vuldtLuSHl4oQUNbVtub3P/mR8b+DbK+Fmp9UZrd5moHP9sD3yeIP4Ms/7iJhdDgLJPKAKNqmA2fsOtg==
"@tiptap/extension-strike@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.14.0.tgz#6c6c802b1e8ffbad22ee4aec9d8068664d4dc2ea"
integrity sha512-rD5d/IL3XPfBOrHRHxt+b+0X1jbIbWONGiad/3sX0ZYQD3PandtCWboH40r/J5tFksebuY12dVYyYQKgLpDBOQ==
"@tiptap/extension-subscript@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.13.0.tgz#deaf6308f89cbcc7b87c1883b4703b389c1c5522"
integrity sha512-C//oecgSn8MNle7k39dSo3z8glNO+4XHX2evZmv6s5NP+88OIZC2GeU4pwKhUFozY8MX3jBWUey1c12IDih/iQ==
"@tiptap/extension-subscript@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.14.0.tgz#b38f4dea8744ff0c698c2917bba8a5f0541a8db6"
integrity sha512-1gQucSZ6WqhKukc8YA7ZfQzBYaVY00F6G7+trD2iWSm6EpiabaUVP0vMjuonIiujTioEwe04KmZuC9ZLbEU9dQ==
"@tiptap/extension-superscript@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.13.0.tgz#2868aef3aa3c1a2547d387680841ead32bbcd217"
integrity sha512-uBeyMqMgHBNDF6GlV7GNajbm7jo0DjCPzgNEKTBh1khJJIyRz4y4O+GiVzApNJmrRnYBAcWWIPbPuwpOcncf5Q==
"@tiptap/extension-superscript@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.14.0.tgz#2e3121ef2c0df2fe5027a69160a107c993da1a47"
integrity sha512-BnsqY9TxN15KxxoX1rulL0sV0Wu3umD4Un0E90LZ5G/QRrVUeohAuOiraqRJ4GnJPVJBR2H0+7Sg5sKqYuIpnQ==
"@tiptap/extension-table-cell@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.13.0.tgz#5b6f4fcc5c896d32569279d7015637607ba5c131"
integrity sha512-7AAKYz0UpJNu7KMVZL+WvPing3LINXfv4+x+hPG5dnRuPjYH4lX5fUhTKoQ9alk1QSxyppX6vB5Z+/PC7gdogw==
"@tiptap/extension-table-cell@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.14.0.tgz#fb28edd5a9f498c7aba851e38dd6e4a7d8663a41"
integrity sha512-DkSNAAkMI/ymPgO8y8Gv0MDVcbd2gk7xrSyicIDNoDFFXp15VasInGW8mvyM+CgvlurGB2N+PkYncPtfb4XNuQ==
"@tiptap/extension-table-header@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.13.0.tgz#dc631ac43b7256096fd9aeba022d5a6da309566f"
integrity sha512-ItnZvVYBWwYXBOw2ubGF037nrw7IODuCCbhAReeaI0DNTgpLeWXEkWbfoiT8gJ1ghlnsxbTCSrj+29/GEclq7g==
"@tiptap/extension-table-header@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.14.0.tgz#10cce2a4b3ace59ffb1734549a09fd13ecd5440d"
integrity sha512-wX6/+t0iCo3KrqK2OjK0vbFeL76Pq+VpobGt+oM8lcxsENnsa6a0s3wdd1QEVLVPlj+WMFQggAG80Rf17+iDxA==
"@tiptap/extension-table-row@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.13.0.tgz#99ed273c6493bfb68d55e4c15893336d931bbbea"
integrity sha512-rV1RVwYACDKRSfygMXVkxiBGW68Gkwop8/SiqXWB2Y/LcQlxgi+S9OsvpouDzIG3H66rqahIXTVbkSO4XuYnyw==
"@tiptap/extension-table-row@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.14.0.tgz#5154a05d199c0728470a99079ca56aae74753a75"
integrity sha512-a1GvCIju9xETIQu664lVQNftHqpPdRmwYp+1QzY82v3zHClso+tTLPeBSlbDdUscSmv3yZXgGML20IiOoR2l2Q==
"@tiptap/extension-table@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.13.0.tgz#bab797d2d63b513d9b986fbde3ba2a974a42ad92"
integrity sha512-f5wYkfJt1T0QVdzSrUsR12Vla2IeJA0UsBgltTfHznLEMzo8asEOn7rgp/YKx4SYgaQfA9L8eqOpoz2kX6e9mA==
"@tiptap/extension-table@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.14.0.tgz#b7541351518c2cf7d5bae72640d6d3375b29c677"
integrity sha512-X/wH3XKxi5+G7cB+lHt3fPMWIJ30IBkzrJZYapJ8d4p2JxMNIU1Nyu+8K6204d0hF6SVWY8hvb/Jq/WgHtoCFA==
"@tiptap/extension-task-item@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.13.0.tgz#efe20ccbbc6f7f8787d758741df354d4b7b90bb7"
integrity sha512-w8GYuQL82i99J/iFaJJiGm4LGL0lfGQwnb8Z0Mi4MrpCqz9Xy5WwgtgD5oDS2ym4aua3w94dpUFPJ+yfAd222Q==
"@tiptap/extension-task-item@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.14.0.tgz#5d576c5486dba9d4475456a3f1dc4e614b6b3ac0"
integrity sha512-MFE928s1J2ACyjOlkx52D/+r6aqz6c516C0tvnP2vzrkijFaSMNY4Xg7L1wTinzIdijh184AYQpyw7LezJa1ug==
"@tiptap/extension-task-list@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.13.0.tgz#931ef7ac570ec5bb928adf491173f57cb921c17d"
integrity sha512-L8ogRow1bjkb4s/Sh42OpzN8sKWRMNDGBoD0jCpRcnRdIsOT+hPhgZfE1GLFwy6+rBYERCHrCQYO0GDBfr+IiA==
"@tiptap/extension-task-list@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.14.0.tgz#a61ae92ac3e534ff192f57c88dd8bbb14ca9fc11"
integrity sha512-o2VELXgkDIHS15pnF1W2OFfxZGvo9V6RcwjzCYUS0mqMF9TTbfHwddRcv4t3pifpMO3sWhspVARavJAGaP5zdQ==
"@tiptap/extension-text@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.13.0.tgz#491654249f608e0ea48c88abe70ac069bd32bbe8"
integrity sha512-4VhKCxfOlBXbaRAM4/OtZnOeq+WcjJoaduo4Jrb7w1I18yqknVjOwFH3Cv+2VqIjPI8bILMWzgpxvFPKdrOllA==
"@tiptap/extension-text@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.14.0.tgz#2b57f47917e97b6c06774b7eaf8598973536a29e"
integrity sha512-rHny566nGZHq61zRLwQ9BPG55W/O+eDKwUJl+LhrLiVWwzpvAl9QQYixtoxJKOY48VK41PKwxe3bgDYgNs/Fhg==
"@tiptap/pm@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.13.0.tgz#a635940264f07898ec8c59afff7ffe3d8989379b"
integrity sha512-CU2DEMQbYXwlnISFD0+7nQSBaqGKJHOiTnWpnAi5pOVphroEIbLPlT6AO4638MWtIR9QsOmGR8KXkQmNGA6tjQ==
"@tiptap/pm@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.14.0.tgz#bc097936a6e4da90dbfe05ff55f8041ff6dda238"
integrity sha512-cnsfaIlvTFCDtLP/A2Fd3LmpttgY0O/tuTM2fC71vetONz83wUTYT+aD9uvxdX0GkSocoh840b0TsEazbBxhpA==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
@ -3418,18 +3418,18 @@
prosemirror-transform "^1.10.2"
prosemirror-view "^1.37.0"
"@tiptap/suggestion@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.13.0.tgz#6654b060d7714b95c67a2a6d5d30b9005fa0a693"
integrity sha512-O+Mwz4vjEURPBYrdlNdelt6Ml6dToZFE+yxXX/sl9UyUiWmej3WAytvIJguycX5RsYGO/Zgan3gk/UCMojY/Kw==
"@tiptap/suggestion@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.14.0.tgz#3170dcf837f9261ecda7c55757ded6c99c977e7c"
integrity sha512-AXzEw0KYIyg5id8gz5geIffnBtkZqan5MWe29rGo3gXTfKH+Ik8tWbZdnlMVheycsUCllrymDRei4zw9DqVqkQ==
"@tiptap/vue-2@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.13.0.tgz#6237cf2fd211619b59e70c0f0381b70d9d3718c4"
integrity sha512-g/qJXHClUVaKxKQhPUKkww/CcRl6zyxYRz64RBxJz78ErPku/tpfvyb/bn+xpoQzmYfDVh/zKv0r7qzk9MLseQ==
"@tiptap/vue-2@^2.14.0":
version "2.14.0"
resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.14.0.tgz#c441eaf789848a2491d32265e2bcbb2f5f370170"
integrity sha512-BD5cRLmQSuEQiMXQ4ngbytZa3D6y8ofc4tBRPHxXKU6bp2CA3Gec6g7LqSiIIeUv/i5r2xfWol7aajypZJL85g==
dependencies:
"@tiptap/extension-bubble-menu" "^2.13.0"
"@tiptap/extension-floating-menu" "^2.13.0"
"@tiptap/extension-bubble-menu" "^2.14.0"
"@tiptap/extension-floating-menu" "^2.14.0"
vue-ts-types "1.6.2"
"@tootallnate/once@2":