Commit c3b1b2a3 authored by Denys Mishunov's avatar Denys Mishunov Committed by Jacques Erasmus

Resolve "Add a keyboard shortcut to quickly open the Web IDE from any repo view"

parent afc46c74
......@@ -306,6 +306,12 @@ export const GO_TO_PROJECT_WIKI = {
defaultKeys: ['g w'], // eslint-disable-line @gitlab/require-i18n-strings
};
export const GO_TO_PROJECT_WEBIDE = {
id: 'project.goToWebIDE',
description: __('Open in Web IDE'),
defaultKeys: ['.'],
};
export const PROJECT_FILES_MOVE_SELECTION_UP = {
id: 'projectFiles.moveSelectionUp',
description: __('Move selection up'),
......@@ -549,6 +555,7 @@ export const PROJECT_SHORTCUTS_GROUP = {
GO_TO_PROJECT_KUBERNETES,
GO_TO_PROJECT_SNIPPETS,
GO_TO_PROJECT_WIKI,
GO_TO_PROJECT_WEBIDE,
],
};
......
import Mousetrap from 'mousetrap';
import { visitUrl, constructWebIDEPath } from '~/lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
import {
keysFor,
......@@ -18,6 +19,7 @@ import {
GO_TO_PROJECT_KUBERNETES,
GO_TO_PROJECT_ENVIRONMENTS,
GO_TO_PROJECT_METRICS,
GO_TO_PROJECT_WEBIDE,
NEW_ISSUE,
} from './keybindings';
import Shortcuts from './shortcuts';
......@@ -58,6 +60,18 @@ export default class ShortcutsNavigation extends Shortcuts {
findAndFollowLink('.shortcuts-environments'),
);
Mousetrap.bind(keysFor(GO_TO_PROJECT_METRICS), () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind(keysFor(GO_TO_PROJECT_WEBIDE), ShortcutsNavigation.navigateToWebIDE);
Mousetrap.bind(keysFor(NEW_ISSUE), () => findAndFollowLink('.shortcuts-new-issue'));
}
static navigateToWebIDE() {
const path = constructWebIDEPath({
sourceProjectFullPath: window.gl.mrWidgetData?.source_project_full_path,
targetProjectFullPath: window.gl.mrWidgetData?.target_project_full_path,
iid: window.gl.mrWidgetData?.iid,
});
if (path) {
visitUrl(path);
}
}
}
......@@ -590,3 +590,30 @@ export function isSameOriginUrl(url) {
return false;
}
}
/**
* Returns a URL to WebIDE considering the current user's position in
* repository's tree. If not MR `iid` has been passed, the URL is fetched
* from the global `gl.webIDEPath`.
*
* @param sourceProjectFullPath Source project's full path. Used in MRs
* @param targetProjectFullPath Target project's full path. Used in MRs
* @param iid MR iid
* @returns {string}
*/
export function constructWebIDEPath({
sourceProjectFullPath,
targetProjectFullPath = '',
iid,
} = {}) {
if (!iid || !sourceProjectFullPath) {
return window.gl?.webIDEPath;
}
return mergeUrlParams(
{
target_project: sourceProjectFullPath !== targetProjectFullPath ? targetProjectFullPath : '',
},
webIDEUrl(`/${sourceProjectFullPath}/merge_requests/${iid}`),
);
}
import { escapeRegExp } from 'lodash';
import Vue from 'vue';
import VueRouter from 'vue-router';
import { joinPaths } from '../lib/utils/url_utility';
import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
import BlobPage from './pages/blob.vue';
import IndexPage from './pages/index.vue';
import TreePage from './pages/tree.vue';
......@@ -24,7 +24,7 @@ export default function createRouter(base, baseRef) {
}),
};
return new VueRouter({
const router = new VueRouter({
mode: 'history',
base: joinPaths(gon.relative_url_root || '', base),
routes: [
......@@ -59,4 +59,21 @@ export default function createRouter(base, baseRef) {
},
],
});
router.afterEach((to) => {
const needsClosingSlash = !to.name.includes('blobPath');
window.gl.webIDEPath = webIDEUrl(
joinPaths(
'/',
base,
'edit',
decodeURI(baseRef),
'-',
to.params.path || '',
needsClosingSlash && '/',
),
);
});
return router;
}
......@@ -10,7 +10,7 @@ import {
GlSafeHtmlDirective as SafeHtml,
GlSprintf,
} from '@gitlab/ui';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import { constructWebIDEPath } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
......@@ -58,15 +58,7 @@ export default {
});
},
webIdePath() {
return mergeUrlParams(
{
target_project:
this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath
? this.mr.targetProjectFullPath
: '',
},
webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`),
);
return constructWebIDEPath(this.mr);
},
isFork() {
return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
......
......@@ -18,3 +18,4 @@
= render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
= render 'shared/web_ide_path'
......@@ -99,3 +99,4 @@
= render 'projects/invite_members_modal', project: @project
- if Gitlab::CurrentSettings.gitpod_enabled && !current_user&.gitpod_enabled
= render 'shared/gitpod/enable_gitpod_modal'
= render 'shared/web_ide_path'
......@@ -11,3 +11,4 @@
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
= render 'shared/web_ide_path'
= javascript_tag do
:plain
window.gl = window.gl || {};
window.gl.webIDEPath = '#{web_ide_url}'
......@@ -23912,6 +23912,9 @@ msgstr ""
msgid "Open errors"
msgstr ""
msgid "Open in Web IDE"
msgstr ""
msgid "Open in file view"
msgstr ""
......
......@@ -1004,4 +1004,39 @@ describe('URL utility', () => {
expect(urlUtils.isSameOriginUrl(url)).toBe(expected);
});
});
describe('constructWebIDEPath', () => {
let originalGl;
const projectIDEPath = '/foo/bar';
const sourceProj = 'my_-fancy-proj/boo';
const targetProj = 'boo/another-fancy-proj';
const mrIid = '7';
beforeEach(() => {
originalGl = window.gl;
window.gl = { webIDEPath: projectIDEPath };
});
afterEach(() => {
window.gl = originalGl;
});
it.each`
sourceProjectFullPath | targetProjectFullPath | iid | expectedPath
${undefined} | ${undefined} | ${undefined} | ${projectIDEPath}
${undefined} | ${undefined} | ${mrIid} | ${projectIDEPath}
${undefined} | ${targetProj} | ${undefined} | ${projectIDEPath}
${undefined} | ${targetProj} | ${mrIid} | ${projectIDEPath}
${sourceProj} | ${undefined} | ${undefined} | ${projectIDEPath}
${sourceProj} | ${targetProj} | ${undefined} | ${projectIDEPath}
${sourceProj} | ${undefined} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`}
${sourceProj} | ${sourceProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`}
${sourceProj} | ${targetProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=${encodeURIComponent(targetProj)}`}
`(
'returns $expectedPath for "$sourceProjectFullPath + $targetProjectFullPath + $iid"',
({ expectedPath, ...args } = {}) => {
expect(urlUtils.constructWebIDEPath(args)).toBe(expectedPath);
},
);
});
});
......@@ -24,4 +24,32 @@ describe('Repository router spec', () => {
expect(componentsForRoute).toContain(component);
}
});
describe('Storing Web IDE path globally', () => {
const proj = 'foo-bar-group/foo-bar-proj';
let originalGl;
beforeEach(() => {
originalGl = window.gl;
});
afterEach(() => {
window.gl = originalGl;
});
it.each`
path | branch | expectedPath
${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`}
${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`}
${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`}
`('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => {
const router = createRouter(proj, branch);
router.push(path);
expect(window.gl.webIDEPath).toBe(expectedPath);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment