Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
f89e368a
Commit
f89e368a
authored
Jan 28, 2020
by
Thomas Randolph
Committed by
Natalia Tepluhina
Jan 28, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
No-Reload URL Permalinks
parent
53c9daeb
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
231 additions
and
5 deletions
+231
-5
app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
+44
-3
app/assets/javascripts/lib/utils/url_utility.js
app/assets/javascripts/lib/utils/url_utility.js
+36
-0
app/assets/javascripts/pages/projects/init_blob.js
app/assets/javascripts/pages/projects/init_blob.js
+1
-0
app/assets/javascripts/repository/utils/title.js
app/assets/javascripts/repository/utils/title.js
+13
-1
changelogs/unreleased/feature-no-reload-permalink.yml
changelogs/unreleased/feature-no-reload-permalink.yml
+6
-0
spec/frontend/lib/utils/url_utility_spec.js
spec/frontend/lib/utils/url_utility_spec.js
+107
-0
spec/frontend/repository/utils/title_spec.js
spec/frontend/repository/utils/title_spec.js
+24
-1
No files found.
app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
View file @
f89e368a
import
Mousetrap
from
'
mousetrap
'
;
import
{
getLocationHash
,
visitUrl
}
from
'
../../lib/utils/url_utility
'
;
import
{
getLocationHash
,
updateHistory
,
urlIsDifferent
,
urlContainsSha
,
getShaFromUrl
,
}
from
'
~/lib/utils/url_utility
'
;
import
{
updateRefPortionOfTitle
}
from
'
~/repository/utils/title
'
;
import
Shortcuts
from
'
./shortcuts
'
;
const
defaults
=
{
skipResetBindings
:
false
,
fileBlobPermalinkUrl
:
null
,
fileBlobPermalinkUrlElement
:
null
,
};
function
eventHasModifierKeys
(
event
)
{
// We ignore alt because I don't think alt clicks normally do anything special?
return
event
.
ctrlKey
||
event
.
metaKey
||
event
.
shiftKey
;
}
export
default
class
ShortcutsBlob
extends
Shortcuts
{
constructor
(
opts
)
{
const
options
=
Object
.
assign
({},
defaults
,
opts
);
super
(
options
.
skipResetBindings
);
this
.
options
=
options
;
this
.
shortcircuitPermalinkButton
();
Mousetrap
.
bind
(
'
y
'
,
this
.
moveToFilePermalink
.
bind
(
this
));
}
moveToFilePermalink
()
{
if
(
this
.
options
.
fileBlobPermalinkUrl
)
{
const
permalink
=
this
.
options
.
fileBlobPermalinkUrl
;
if
(
permalink
)
{
const
hash
=
getLocationHash
();
const
hashUrlString
=
hash
?
`#
${
hash
}
`
:
''
;
visitUrl
(
`
${
this
.
options
.
fileBlobPermalinkUrl
}${
hashUrlString
}
`
);
if
(
urlIsDifferent
(
permalink
))
{
updateHistory
({
url
:
`
${
permalink
}${
hashUrlString
}
`
,
title
:
document
.
title
,
});
}
if
(
urlContainsSha
({
url
:
permalink
}))
{
updateRefPortionOfTitle
(
getShaFromUrl
({
url
:
permalink
}));
}
}
}
shortcircuitPermalinkButton
()
{
const
button
=
this
.
options
.
fileBlobPermalinkUrlElement
;
const
handleButton
=
e
=>
{
if
(
!
eventHasModifierKeys
(
e
))
{
e
.
preventDefault
();
this
.
moveToFilePermalink
();
}
};
if
(
button
)
{
button
.
addEventListener
(
'
click
'
,
handleButton
);
}
}
}
app/assets/javascripts/lib/utils/url_utility.js
View file @
f89e368a
const
PATH_SEPARATOR
=
'
/
'
;
const
PATH_SEPARATOR_LEADING_REGEX
=
new
RegExp
(
`^
${
PATH_SEPARATOR
}
+`
);
const
PATH_SEPARATOR_ENDING_REGEX
=
new
RegExp
(
`
${
PATH_SEPARATOR
}
+$`
);
const
SHA_REGEX
=
/
[\d
a-f
]{40}
/gi
;
// Reset the cursor in a Regex so that multiple uses before a recompile don't fail
function
resetRegExp
(
regex
)
{
regex
.
lastIndex
=
0
;
/* eslint-disable-line no-param-reassign */
return
regex
;
}
// Returns a decoded url parameter value
// - Treats '+' as '%20'
...
...
@@ -128,6 +136,20 @@ export function doesHashExistInUrl(hashName) {
return
hash
&&
hash
.
includes
(
hashName
);
}
export
function
urlContainsSha
({
url
=
String
(
window
.
location
)
}
=
{})
{
return
resetRegExp
(
SHA_REGEX
).
test
(
url
);
}
export
function
getShaFromUrl
({
url
=
String
(
window
.
location
)
}
=
{})
{
let
sha
=
null
;
if
(
urlContainsSha
({
url
}))
{
[
sha
]
=
url
.
match
(
resetRegExp
(
SHA_REGEX
));
}
return
sha
;
}
/**
* Apply the fragment to the given url by returning a new url string that includes
* the fragment. If the given url already contains a fragment, the original fragment
...
...
@@ -154,6 +176,16 @@ export function visitUrl(url, external = false) {
}
}
export
function
updateHistory
({
state
=
{},
title
=
''
,
url
,
replace
=
false
,
win
=
window
}
=
{})
{
if
(
win
.
history
)
{
if
(
replace
)
{
win
.
history
.
replaceState
(
state
,
title
,
url
);
}
else
{
win
.
history
.
pushState
(
state
,
title
,
url
);
}
}
}
export
function
refreshCurrentPage
()
{
visitUrl
(
window
.
location
.
href
);
}
...
...
@@ -282,3 +314,7 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
};
export
const
escapeFileUrl
=
fileUrl
=>
encodeURIComponent
(
fileUrl
).
replace
(
/%2F/g
,
'
/
'
);
export
function
urlIsDifferent
(
url
,
compare
=
String
(
window
.
location
))
{
return
url
!==
compare
;
}
app/assets/javascripts/pages/projects/init_blob.js
View file @
f89e368a
...
...
@@ -25,6 +25,7 @@ export default () => {
new
ShortcutsBlob
({
skipResetBindings
:
true
,
fileBlobPermalinkUrl
,
fileBlobPermalinkUrlElement
,
});
new
BlobForkSuggestion
({
...
...
app/assets/javascripts/repository/utils/title.js
View file @
f89e368a
const
DEFAULT_TITLE
=
'
· GitLab
'
;
// eslint-disable-next-line import/prefer-default-export
export
const
setTitle
=
(
pathMatch
,
ref
,
project
)
=>
{
if
(
!
pathMatch
)
{
document
.
title
=
`
${
project
}
${
DEFAULT_TITLE
}
`
;
...
...
@@ -12,3 +12,15 @@ export const setTitle = (pathMatch, ref, project) => {
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
document
.
title
=
`
${
isEmpty
?
'
Files
'
:
path
}
·
${
ref
}
·
${
project
}
${
DEFAULT_TITLE
}
`
;
};
export
function
updateRefPortionOfTitle
(
sha
,
doc
=
document
)
{
const
{
title
=
''
}
=
doc
;
const
titleParts
=
title
.
split
(
'
·
'
);
if
(
titleParts
.
length
>
1
)
{
titleParts
[
1
]
=
sha
;
/* eslint-disable-next-line no-param-reassign */
doc
.
title
=
titleParts
.
join
(
'
·
'
);
}
}
changelogs/unreleased/feature-no-reload-permalink.yml
0 → 100644
View file @
f89e368a
---
title
:
When switching to a file permalink, just change the URL instead of triggering
a useless page reload
merge_request
:
22340
author
:
type
:
added
spec/frontend/lib/utils/url_utility_spec.js
View file @
f89e368a
import
*
as
urlUtils
from
'
~/lib/utils/url_utility
'
;
const
shas
=
{
valid
:
[
'
ad9be38573f9ee4c4daec22673478c2dd1d81cd8
'
,
'
76e07a692f65a2f4fd72f107a3e83908bea9b7eb
'
,
'
9dd8f215b1e8605b1d59eaf9df1178081cda0aaf
'
,
'
f2e0be58c4091b033203bae1cc0302febd54117d
'
,
],
invalid
:
[
'
zd9be38573f9ee4c4daec22673478c2dd1d81cd8
'
,
'
:6e07a692f65a2f4fd72f107a3e83908bea9b7eb
'
,
'
-dd8f215b1e8605b1d59eaf9df1178081cda0aaf
'
,
'
2e0be58c4091b033203bae1cc0302febd54117d
'
,
],
};
const
setWindowLocation
=
value
=>
{
Object
.
defineProperty
(
window
,
'
location
'
,
{
writable
:
true
,
...
...
@@ -154,6 +169,44 @@ describe('URL utility', () => {
});
});
describe
(
'
urlContainsSha
'
,
()
=>
{
it
(
'
returns true when there is a valid 40-character SHA1 hash in the URL
'
,
()
=>
{
shas
.
valid
.
forEach
(
sha
=>
{
expect
(
urlUtils
.
urlContainsSha
({
url
:
`http://urlstuff/
${
sha
}
/moreurlstuff`
}),
).
toBeTruthy
();
});
});
it
(
'
returns false when there is not a valid 40-character SHA1 hash in the URL
'
,
()
=>
{
shas
.
invalid
.
forEach
(
str
=>
{
expect
(
urlUtils
.
urlContainsSha
({
url
:
`http://urlstuff/
${
str
}
/moreurlstuff`
})).
toBeFalsy
();
});
});
});
describe
(
'
getShaFromUrl
'
,
()
=>
{
let
validUrls
=
[];
let
invalidUrls
=
[];
beforeAll
(()
=>
{
validUrls
=
shas
.
valid
.
map
(
sha
=>
`http://urlstuff/
${
sha
}
/moreurlstuff`
);
invalidUrls
=
shas
.
invalid
.
map
(
str
=>
`http://urlstuff/
${
str
}
/moreurlstuff`
);
});
it
(
'
returns the valid 40-character SHA1 hash from the URL
'
,
()
=>
{
validUrls
.
forEach
((
url
,
idx
)
=>
{
expect
(
urlUtils
.
getShaFromUrl
({
url
})).
toBe
(
shas
.
valid
[
idx
]);
});
});
it
(
'
returns null from a URL with no valid 40-character SHA1 hash
'
,
()
=>
{
invalidUrls
.
forEach
(
url
=>
{
expect
(
urlUtils
.
getShaFromUrl
({
url
})).
toBeNull
();
});
});
});
describe
(
'
setUrlFragment
'
,
()
=>
{
it
(
'
should set fragment when url has no fragment
'
,
()
=>
{
const
url
=
urlUtils
.
setUrlFragment
(
'
/home/feature
'
,
'
usage
'
);
...
...
@@ -174,6 +227,44 @@ describe('URL utility', () => {
});
});
describe
(
'
updateHistory
'
,
()
=>
{
const
state
=
{
key
:
'
prop
'
};
const
title
=
'
TITLE
'
;
const
url
=
'
URL
'
;
const
win
=
{
history
:
{
pushState
:
jest
.
fn
(),
replaceState
:
jest
.
fn
(),
},
};
beforeEach
(()
=>
{
win
.
history
.
pushState
.
mockReset
();
win
.
history
.
replaceState
.
mockReset
();
});
it
(
'
should call replaceState if the replace option is true
'
,
()
=>
{
urlUtils
.
updateHistory
({
state
,
title
,
url
,
replace
:
true
,
win
});
expect
(
win
.
history
.
replaceState
).
toHaveBeenCalledWith
(
state
,
title
,
url
);
expect
(
win
.
history
.
pushState
).
not
.
toHaveBeenCalled
();
});
it
(
'
should call pushState if the replace option is missing
'
,
()
=>
{
urlUtils
.
updateHistory
({
state
,
title
,
url
,
win
});
expect
(
win
.
history
.
replaceState
).
not
.
toHaveBeenCalled
();
expect
(
win
.
history
.
pushState
).
toHaveBeenCalledWith
(
state
,
title
,
url
);
});
it
(
'
should call pushState if the replace option is false
'
,
()
=>
{
urlUtils
.
updateHistory
({
state
,
title
,
url
,
replace
:
false
,
win
});
expect
(
win
.
history
.
replaceState
).
not
.
toHaveBeenCalled
();
expect
(
win
.
history
.
pushState
).
toHaveBeenCalledWith
(
state
,
title
,
url
);
});
});
describe
(
'
getBaseURL
'
,
()
=>
{
beforeEach
(()
=>
{
setWindowLocation
({
...
...
@@ -331,6 +422,22 @@ describe('URL utility', () => {
});
});
describe
(
'
urlIsDifferent
'
,
()
=>
{
beforeEach
(()
=>
{
setWindowLocation
(
'
current
'
);
});
it
(
'
should compare against the window location if no compare value is provided
'
,
()
=>
{
expect
(
urlUtils
.
urlIsDifferent
(
'
different
'
)).
toBeTruthy
();
expect
(
urlUtils
.
urlIsDifferent
(
'
current
'
)).
toBeFalsy
();
});
it
(
'
should use the provided compare value
'
,
()
=>
{
expect
(
urlUtils
.
urlIsDifferent
(
'
different
'
,
'
current
'
)).
toBeTruthy
();
expect
(
urlUtils
.
urlIsDifferent
(
'
current
'
,
'
current
'
)).
toBeFalsy
();
});
});
describe
(
'
setUrlParams
'
,
()
=>
{
it
(
'
adds new params as query string
'
,
()
=>
{
const
url
=
'
https://gitlab.com/test
'
;
...
...
spec/frontend/repository/utils/title_spec.js
View file @
f89e368a
import
{
setTitle
}
from
'
~/repository/utils/title
'
;
import
{
setTitle
,
updateRefPortionOfTitle
}
from
'
~/repository/utils/title
'
;
describe
(
'
setTitle
'
,
()
=>
{
it
.
each
`
...
...
@@ -13,3 +13,26 @@ describe('setTitle', () => {
expect
(
document
.
title
).
toEqual
(
`
${
title
}
· master · GitLab Org / GitLab · GitLab`
);
});
});
describe
(
'
updateRefPortionOfTitle
'
,
()
=>
{
const
sha
=
'
abc
'
;
const
testCases
=
[
[
'
updates the title with the SHA
'
,
{
title
:
'
part 1 · part 2 · part 3
'
},
'
part 1 · abc · part 3
'
,
],
[
"
makes no change if there's no title
"
,
{
foo
:
null
},
undefined
],
[
"
makes no change if the title doesn't split predictably
"
,
{
title
:
'
part 1 - part 2 - part 3
'
},
'
part 1 - part 2 - part 3
'
,
],
];
it
.
each
(
testCases
)(
'
%s
'
,
(
desc
,
doc
,
title
)
=>
{
updateRefPortionOfTitle
(
sha
,
doc
);
expect
(
doc
.
title
).
toEqual
(
title
);
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment