Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-13 21:09:34 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-13 21:09:34 +0300
commit53d77359a0e6bf78bfc8ef8c72995eebe1f9e63b (patch)
tree444eb2851a18271c20ea6fd23fab0b02b7e4d9b2 /app
parentdd7da8bd31926a1210011856f7d66ee43fa65156 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue28
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue140
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue15
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue16
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_link_button.vue125
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js16
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js6
-rw-r--r--app/assets/javascripts/lib/utils/vue3compat/vue_router.js105
-rw-r--r--app/assets/javascripts/pages/projects/project.js139
-rw-r--r--app/views/shared/_ref_switcher.html.haml22
11 files changed, 277 insertions, 341 deletions
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index f92c06956a8..020edcb01b8 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -5,6 +5,7 @@ import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.v
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import { titleQueries } from 'ee_else_ce/boards/constants';
export default {
components: {
@@ -19,7 +20,7 @@ export default {
directives: {
autofocusonshow,
},
- inject: ['isApolloBoard'],
+ inject: ['fullPath', 'issuableType', 'isEpicBoard', 'isApolloBoard'],
props: {
activeItem: {
type: Object,
@@ -77,8 +78,9 @@ export default {
},
async setPendingState() {
const pendingChanges = localStorage.getItem(this.pendingChangesStorageKey);
+ const shouldOpen = pendingChanges !== this.title;
- if (pendingChanges) {
+ if (pendingChanges && shouldOpen) {
this.title = pendingChanges;
this.showChangesAlert = true;
await this.$nextTick();
@@ -93,6 +95,26 @@ export default {
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
},
+ async setActiveBoardItemTitle() {
+ if (!this.isApolloBoard) {
+ await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
+ return;
+ }
+ const { fullPath, issuableType, isEpicBoard, title } = this;
+ const workspacePath = isEpicBoard
+ ? { groupPath: fullPath }
+ : { projectPath: this.projectPath };
+ await this.$apollo.mutate({
+ mutation: titleQueries[issuableType].mutation,
+ variables: {
+ input: {
+ ...workspacePath,
+ iid: String(this.item.iid),
+ title,
+ },
+ },
+ });
+ },
async setTitle() {
this.$refs.sidebarItem.collapse();
@@ -102,7 +124,7 @@ export default {
try {
this.loading = true;
- await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
+ await this.setActiveBoardItemTitle();
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
index 06b80a65528..cef446c4cf8 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
@@ -35,9 +35,6 @@ export default {
);
},
},
- toggleLinkCommandParams: {
- href: '',
- },
};
</script>
<template>
@@ -122,8 +119,7 @@ export default {
data-testid="link"
content-type="link"
icon-name="link"
- editor-command="toggleLink"
- :editor-command-params="$options.toggleLinkCommandParams"
+ editor-command="editLink"
category="tertiary"
size="medium"
:label="__('Insert link')"
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
index a4713eb3275..a3065be3772 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue
@@ -8,6 +8,7 @@ import {
GlButtonGroup,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
+import { getMarkType, getMarkRange } from '@tiptap/core';
import Link from '../../extensions/link';
import EditorStateObserver from '../editor_state_observer.vue';
import BubbleMenu from './bubble_menu.vue';
@@ -31,12 +32,36 @@ export default {
return {
linkHref: undefined,
linkCanonicalSrc: undefined,
- linkTitle: undefined,
+ linkText: undefined,
isEditing: false,
};
},
methods: {
+ linkIsEmpty() {
+ return (
+ !this.linkCanonicalSrc &&
+ !this.linkHref &&
+ (!this.linkText || this.linkText === this.linkTextInDoc())
+ );
+ },
+
+ linkTextInDoc() {
+ const { state } = this.tiptapEditor;
+ const type = getMarkType(Link.name, state.schema);
+ let { selection: range } = state;
+ if (range.from === range.to) {
+ range =
+ getMarkRange(state.selection.$from, type) ||
+ getMarkRange(state.selection.$to, type) ||
+ {};
+ }
+
+ if (!range.from || !range.to) return '';
+
+ return state.doc.textBetween(range.from, range.to, ' ');
+ },
+
shouldShow() {
return this.tiptapEditor.isActive(Link.name);
},
@@ -52,31 +77,51 @@ export default {
this.isEditing = false;
this.linkHref = await this.contentEditor.resolveUrl(this.linkCanonicalSrc);
-
- if (!this.linkCanonicalSrc && !this.linkHref) {
- this.removeLink();
- }
},
cancelEditingLink() {
this.endEditingLink();
- this.updateLinkToState();
+
+ if (this.linkIsEmpty()) {
+ this.removeLink();
+ } else {
+ this.updateLinkToState();
+ }
},
async saveEditedLink() {
- if (!this.linkCanonicalSrc) {
+ const chain = this.tiptapEditor.chain().focus();
+
+ const attrs = {
+ href: this.linkCanonicalSrc,
+ canonicalSrc: this.linkCanonicalSrc,
+ };
+
+ // if nothing was entered by the user and the link is empty, remove it
+ // since we don't want to insert an empty link
+ if (this.linkIsEmpty()) {
this.removeLink();
- } else {
- this.tiptapEditor
- .chain()
- .focus()
+ return;
+ }
+
+ if (!this.linkText) {
+ this.linkText = this.linkCanonicalSrc;
+ }
+
+ // if link text was updated, insert a new link in the doc with the new text
+ if (this.linkTextInDoc() !== this.linkText) {
+ chain
.extendMarkRange(Link.name)
- .updateAttributes(Link.name, {
- href: this.linkCanonicalSrc,
- canonicalSrc: this.linkCanonicalSrc,
- title: this.linkTitle,
+ .setMeta('preventAutolink', true)
+ .insertContent({
+ marks: [{ type: Link.name, attrs }],
+ type: 'text',
+ text: this.linkText,
})
.run();
+ } else {
+ // if link text was not updated, just update the attributes
+ chain.updateAttributes(Link.name, attrs).run();
}
this.endEditingLink();
@@ -84,22 +129,27 @@ export default {
updateLinkToState() {
const editor = this.tiptapEditor;
-
- const { href, title, canonicalSrc } = editor.getAttributes(Link.name);
+ const { href, canonicalSrc } = editor.getAttributes(Link.name);
+ const text = this.linkTextInDoc();
if (
canonicalSrc === this.linkCanonicalSrc &&
href === this.linkHref &&
- title === this.linkTitle
+ text === this.linkText
) {
return;
}
- this.linkTitle = title;
+ this.linkText = text;
this.linkHref = href;
this.linkCanonicalSrc = canonicalSrc || href;
+ },
- this.isEditing = !this.linkCanonicalSrc;
+ onTransaction({ transaction }) {
+ this.linkText = this.linkTextInDoc();
+ if (transaction.getMeta('creatingLink')) {
+ this.isEditing = true;
+ }
},
copyLinkHref() {
@@ -107,31 +157,49 @@ export default {
},
removeLink() {
- this.tiptapEditor.chain().focus().extendMarkRange(Link.name).unsetLink().run();
+ const chain = this.tiptapEditor.chain().focus();
+ if (this.linkTextInDoc()) {
+ chain.unsetLink().run();
+ } else {
+ chain
+ .insertContent({
+ type: 'text',
+ text: ' ',
+ })
+ .extendMarkRange(Link.name)
+ .unsetLink()
+ .deleteSelection()
+ .run();
+ }
},
resetBubbleMenuState() {
- this.linkTitle = undefined;
+ this.linkText = undefined;
this.linkHref = undefined;
this.linkCanonicalSrc = undefined;
},
},
tippyOptions: {
placement: 'bottom',
+ appendTo: () => document.body,
},
};
</script>
<template>
- <bubble-menu
- data-testid="link-bubble-menu"
- class="gl-shadow gl-rounded-base gl-bg-white"
- plugin-key="bubbleMenuLink"
- :should-show="shouldShow"
- :tippy-options="$options.tippyOptions"
- @show="updateLinkToState"
- @hidden="resetBubbleMenuState"
+ <editor-state-observer
+ :debounce="0"
+ @transaction="onTransaction"
+ @selectionUpdate="updateLinkToState"
>
- <editor-state-observer @selectionUpdate="updateLinkToState">
+ <bubble-menu
+ data-testid="link-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ plugin-key="bubbleMenuLink"
+ :should-show="shouldShow"
+ :tippy-options="$options.tippyOptions"
+ @show="updateLinkToState"
+ @hidden="resetBubbleMenuState"
+ >
<gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">
<gl-link
v-gl-tooltip
@@ -178,12 +246,12 @@ export default {
/>
</gl-button-group>
<gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedLink">
+ <gl-form-group :label="__('Text')" label-for="link-text">
+ <gl-form-input id="link-text" v-model="linkText" data-testid="link-text" />
+ </gl-form-group>
<gl-form-group :label="__('URL')" label-for="link-href">
<gl-form-input id="link-href" v-model="linkCanonicalSrc" data-testid="link-href" />
</gl-form-group>
- <gl-form-group :label="__('Title')" label-for="link-title">
- <gl-form-input id="link-title" v-model="linkTitle" data-testid="link-title" />
- </gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button class="gl-mr-3" data-testid="cancel-link" @click="cancelEditingLink">
{{ __('Cancel') }}
@@ -193,6 +261,6 @@ export default {
</gl-button>
</div>
</gl-form>
- </editor-state-observer>
- </bubble-menu>
+ </bubble-menu>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index ccb46e3b593..62f2113a8f4 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -16,14 +16,21 @@ const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEv
export default {
inject: ['tiptapEditor', 'eventHub'],
+ props: {
+ debounce: {
+ type: Number,
+ required: false,
+ default: 100,
+ },
+ },
created() {
this.disposables = [];
Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
- const eventHandler = debounce(
- (params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params),
- 100,
- );
+ let eventHandler = (params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params);
+ if (this.debounce) {
+ eventHandler = debounce(eventHandler, this.debounce);
+ }
this.tiptapEditor?.on(tiptapEvent, eventHandler);
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 11b3a5a3069..230141fe1c1 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -2,7 +2,6 @@
import trackUIControl from '../services/track_ui_control';
import ToolbarButton from './toolbar_button.vue';
import ToolbarAttachmentButton from './toolbar_attachment_button.vue';
-import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
import ToolbarMoreDropdown from './toolbar_more_dropdown.vue';
@@ -11,7 +10,6 @@ export default {
components: {
ToolbarButton,
ToolbarTextStyleDropdown,
- ToolbarLinkButton,
ToolbarTableButton,
ToolbarAttachmentButton,
ToolbarMoreDropdown,
@@ -24,7 +22,10 @@ export default {
};
</script>
<template>
- <div class="gl-w-full gl-border-b gl-display-flex gl-justify-content-end">
+ <div
+ class="gl-w-full gl-border-b gl-display-flex gl-justify-content-end"
+ data-testid="formatting-toolbar"
+ >
<div class="gl-py-2 gl-display-flex gl-flex-wrap-wrap gl-align-items-end">
<toolbar-text-style-dropdown
data-testid="text-styles"
@@ -62,7 +63,14 @@ export default {
:label="__('Code')"
@execute="trackToolbarControlExecution"
/>
- <toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" />
+ <toolbar-button
+ data-testid="link"
+ content-type="link"
+ icon-name="link"
+ editor-command="editLink"
+ :label="__('Insert link')"
+ @execute="trackToolbarControlExecution"
+ />
<toolbar-button
data-testid="bullet-list"
content-type="bulletList"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
deleted file mode 100644
index ff1a52264f0..00000000000
--- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownForm,
- GlButton,
- GlFormInputGroup,
- GlDropdownDivider,
- GlDropdownItem,
- GlTooltipDirective as GlTooltip,
-} from '@gitlab/ui';
-import Link from '../extensions/link';
-import { hasSelection } from '../services/utils';
-import EditorStateObserver from './editor_state_observer.vue';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownForm,
- GlFormInputGroup,
- GlDropdownDivider,
- GlDropdownItem,
- GlButton,
- EditorStateObserver,
- },
- directives: {
- GlTooltip,
- },
- inject: ['tiptapEditor'],
- data() {
- return {
- linkHref: '',
- isActive: false,
- };
- },
- methods: {
- resetFields() {
- this.imgSrc = '';
- this.$refs.fileSelector.value = '';
- },
- updateLinkState({ editor }) {
- const { canonicalSrc, href } = editor.getAttributes(Link.name);
-
- this.isActive = editor.isActive(Link.name);
- this.linkHref = canonicalSrc || href;
- },
- updateLink() {
- this.tiptapEditor
- .chain()
- .focus()
- .unsetLink()
- .setLink({
- href: this.linkHref,
- canonicalSrc: this.linkHref,
- })
- .run();
-
- this.$emit('execute', { contentType: Link.name });
- },
- selectLink() {
- const { tiptapEditor } = this;
-
- // a selection has already been made by the user, so do nothing
- if (!hasSelection(tiptapEditor)) {
- tiptapEditor.chain().focus().extendMarkRange(Link.name).run();
- }
- },
- removeLink() {
- this.tiptapEditor.chain().focus().unsetLink().run();
-
- this.$emit('execute', { contentType: Link.name });
- },
- onFileSelect(e) {
- this.tiptapEditor
- .chain()
- .focus()
- .uploadAttachment({
- file: e.target.files[0],
- })
- .run();
-
- this.resetFields();
- this.$emit('execute', { contentType: Link.name });
- },
- },
-};
-</script>
-<template>
- <editor-state-observer @transaction="updateLinkState">
- <span class="gl-display-inline-flex">
- <gl-dropdown
- v-gl-tooltip
- :title="__('Insert link')"
- :text="__('Insert link')"
- :toggle-class="{ active: isActive }"
- size="small"
- category="tertiary"
- icon="link"
- text-sr-only
- lazy
- @show="selectLink()"
- >
- <gl-dropdown-form class="gl-px-3!" :class="{ 'gl-pb-2!': isActive }">
- <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
- <template #append>
- <gl-button variant="confirm" @click="updateLink">{{ __('Apply') }}</gl-button>
- </template>
- </gl-form-input-group>
- </gl-dropdown-form>
- <div v-if="isActive">
- <gl-dropdown-divider />
- <gl-dropdown-item @click="removeLink">
- {{ __('Remove link') }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
- <input
- ref="fileSelector"
- type="file"
- name="content_editor_attachment"
- class="gl-display-none"
- @change="onFileSelect"
- />
- </span>
- </editor-state-observer>
-</template>
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index e985e561fda..314d5230b01 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -18,6 +18,8 @@ export const extractHrefFromMarkdownLink = (match) => {
};
export default Link.extend({
+ inclusive: false,
+
addOptions() {
return {
...this.parent?.(),
@@ -64,4 +66,18 @@ export default Link.extend({
},
};
},
+ addCommands() {
+ return {
+ ...this.parent?.(),
+ editLink: (attrs) => ({ chain }) => {
+ chain().setMeta('creatingLink', true).setLink(attrs).run();
+ },
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ 'Mod-k': () => this.editor.commands.editLink(),
+ };
+ },
});
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index ab8cf7014d9..664473fccfe 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -607,7 +607,7 @@ export const link = {
return '[';
}
- const attrs = { href: state.esc(href || canonicalSrc) };
+ const attrs = { href: state.esc(href || canonicalSrc || '') };
if (title) {
attrs.title = title;
@@ -623,14 +623,14 @@ export const link = {
const { canonicalSrc, href, title, sourceMarkdown, isReference } = mark.attrs;
if (isReference) {
- return `][${state.esc(canonicalSrc || href)}]`;
+ return `][${state.esc(canonicalSrc || href || '')}]`;
}
if (linkType(sourceMarkdown) === LINK_HTML) {
return closeTag('a');
}
- return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`;
+ return `](${state.esc(canonicalSrc || href || '')}${title ? ` ${state.quote(title)}` : ''})`;
},
};
diff --git a/app/assets/javascripts/lib/utils/vue3compat/vue_router.js b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js
new file mode 100644
index 00000000000..006ed920ef0
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js
@@ -0,0 +1,105 @@
+import Vue from 'vue';
+import {
+ createRouter,
+ createMemoryHistory,
+ createWebHistory,
+ createWebHashHistory,
+} from 'vue-router-vue3';
+
+const mode = (value, options) => {
+ if (!value) return null;
+ let history;
+ // eslint-disable-next-line default-case
+ switch (value) {
+ case 'history':
+ history = createWebHistory(options.base);
+ break;
+ case 'hash':
+ history = createWebHashHistory();
+ break;
+ case 'abstract':
+ history = createMemoryHistory();
+ break;
+ }
+ return { history };
+};
+
+const base = () => null;
+
+const toNewCatchAllPath = (path) => {
+ if (path === '*') return '/:pathMatch(.*)*';
+ return path;
+};
+
+const routes = (value) => {
+ if (!value) return null;
+ const newRoutes = value.reduce(function handleRoutes(acc, route) {
+ const newRoute = {
+ ...route,
+ path: toNewCatchAllPath(route.path),
+ };
+ if (route.children) {
+ newRoute.children = route.children.reduce(handleRoutes, []);
+ }
+ acc.push(newRoute);
+ return acc;
+ }, []);
+ return { routes: newRoutes };
+};
+
+const scrollBehavior = (value) => {
+ return {
+ scrollBehavior(...args) {
+ const { x, y, left, top } = value(...args);
+ return { left: x || left, top: y || top };
+ },
+ };
+};
+
+const transformers = {
+ mode,
+ base,
+ routes,
+ scrollBehavior,
+};
+
+const transformOptions = (options) => {
+ const defaultConfig = {
+ routes: null,
+ history: createWebHashHistory(),
+ };
+ return Object.keys(options).reduce((acc, key) => {
+ const value = options[key];
+ if (key in transformers) {
+ Object.assign(acc, transformers[key](value, options));
+ } else {
+ acc[key] = value;
+ }
+ return acc;
+ }, defaultConfig);
+};
+
+const installed = new WeakMap();
+
+export default class VueRouterCompat {
+ constructor(options) {
+ // eslint-disable-next-line no-constructor-return
+ return createRouter(transformOptions(options));
+ }
+
+ static install() {
+ Vue.mixin({
+ beforeCreate() {
+ const { app } = this.$.appContext;
+ const { router } = this.$options;
+ if (router && !installed.get(app)?.has(router)) {
+ if (!installed.has(app)) {
+ installed.set(app, new WeakSet());
+ }
+ installed.get(app).add(router);
+ this.$.appContext.app.use(this.$options.router);
+ }
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 5f15a11e708..e6ee6b702bb 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -3,28 +3,10 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import initClonePanel from '~/clone_panel';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { serializeForm } from '~/lib/utils/forms';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-
-const BRANCH_REF_TYPE = 'heads';
-const TAG_REF_TYPE = 'tags';
-const BRANCH_GROUP_NAME = __('Branches');
-const TAG_GROUP_NAME = __('Tags');
export default class Project {
constructor() {
initClonePanel();
- // Ref switcher
- if (document.querySelector('.js-project-refs-dropdown')) {
- Project.initRefSwitcher();
- $('.project-refs-select').on('change', function () {
- return $(this).parents('form').trigger('submit');
- });
- }
$('.js-hide-no-ssh-message').on('click', function (e) {
setCookie('hide_no_ssh_message', 'false');
@@ -48,125 +30,4 @@ export default class Project {
static changeProject(url) {
return (window.location = url);
}
-
- static initRefSwitcher() {
- const refListItem = document.createElement('li');
- const refLink = document.createElement('a');
-
- refLink.href = '#';
-
- return $('.js-project-refs-dropdown').each(function () {
- const $dropdown = $(this);
- const selected = $dropdown.data('selected');
- const refType = $dropdown.data('refType');
- const fieldName = $dropdown.data('fieldName');
- const shouldVisit = Boolean($dropdown.data('visit'));
- const $form = $dropdown.closest('form');
- const path = $form.find('#path').val();
- const action = $form.attr('action');
- const linkTarget = mergeUrlParams(serializeForm($form[0]), action);
-
- return initDeprecatedJQueryDropdown($dropdown, {
- data(term, callback) {
- axios
- .get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- })
- .then(({ data }) => callback(data))
- .catch(() =>
- createAlert({
- message: __('An error occurred while getting projects'),
- }),
- );
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- filterByText: true,
- inputFieldName: $dropdown.data('inputFieldName'),
- fieldName,
- renderRow(ref, _, params) {
- const li = refListItem.cloneNode(false);
-
- const link = refLink.cloneNode(false);
-
- if (ref === selected) {
- // Check group and current ref type to avoid adding a class when tags and branches share the same name
- if (
- (refType === BRANCH_REF_TYPE && params.group === BRANCH_GROUP_NAME) ||
- (refType === TAG_REF_TYPE && params.group === TAG_GROUP_NAME) ||
- !refType
- ) {
- link.className = 'is-active';
- }
- }
-
- link.textContent = ref;
- link.dataset.ref = ref;
- if (ref.length > 0 && shouldVisit) {
- const urlParams = { [fieldName]: ref };
- if (params.group === BRANCH_GROUP_NAME) {
- urlParams.ref_type = BRANCH_REF_TYPE;
- } else {
- urlParams.ref_type = TAG_REF_TYPE;
- }
- link.href = mergeUrlParams(urlParams, linkTarget);
- }
-
- li.appendChild(link);
-
- return li;
- },
- id(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel(obj, $el) {
- return $el.text().trim();
- },
- clicked(options) {
- const { e } = options;
-
- if (!shouldVisit) {
- e.preventDefault();
- }
-
- // Some pages need to dynamically get the current path
- // so they can opt-in to JS getting the path from the
- // current URL by not setting a path in the dropdown form
- if (shouldVisit && path === undefined) {
- e.preventDefault();
-
- const selectedUrl = new URL(e.target.href);
- const loc = window.location.href;
-
- if (loc.includes('/-/')) {
- const currentRef = $dropdown.data('ref');
- // The split and startWith is to ensure an exact word match
- // and avoid partial match ie. currentRef is "dev" and loc is "development"
- const splitPathAfterRefPortion = loc.split('/-/')[1].split(currentRef)[1];
- const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/');
-
- if (doesPathContainRef) {
- // We are ignoring the url containing the ref portion
- // and plucking the thereafter portion to reconstructure the url that is correct
- const targetPath = splitPathAfterRefPortion?.slice(1).split('#')[0].split('?')[0];
- selectedUrl.searchParams.set('path', targetPath);
- selectedUrl.hash = window.location.hash;
- }
- }
-
- // Open in new window if "meta" key is pressed
- if (e.metaKey) {
- window.open(selectedUrl.href, '_blank');
- } else {
- window.location.href = selectedUrl.href;
- }
- }
- },
- });
- });
- }
}
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
deleted file mode 100644
index 48633ce0ce7..00000000000
--- a/app/views/shared/_ref_switcher.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-- return unless @project
-
-- ref = local_assigns.fetch(:ref, @ref)
-- form_path = local_assigns.fetch(:form_path, switch_project_refs_path(@project))
-- dropdown_toggle_text = ref || @project.default_branch
-- field_name = local_assigns.fetch(:field_name, 'ref')
-
-= form_tag form_path, method: :get, class: "project-refs-form" do
- - if defined?(destination)
- = hidden_field_tag :destination, destination
- - if defined?(path)
- = hidden_field_tag :path, path
- - @options && @options.each do |key, value|
- = hidden_field_tag key, value, id: nil
- .dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, ref_type: @ref_type, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
- .dropdown-page-one
- = dropdown_title _("Switch branch/tag")
- = dropdown_filter _("Search branches and tags")
- = dropdown_content
- = dropdown_loading