Commit b5ece117 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into '3843-add-option-to-push-mirror-only-protected-branches'

# Conflicts:
#   db/schema.rb
parents 806fc946 67d4beae
...@@ -607,7 +607,7 @@ codequality: ...@@ -607,7 +607,7 @@ codequality:
script: script:
- cp .rubocop.yml .rubocop.yml.bak - cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml - mv .rubocop.yml.bak .rubocop.yml
artifacts: artifacts:
......
...@@ -414,7 +414,7 @@ group :ed25519 do ...@@ -414,7 +414,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -298,7 +298,7 @@ GEM ...@@ -298,7 +298,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.51.0) gitaly-proto (0.52.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1061,7 +1061,7 @@ DEPENDENCIES ...@@ -1061,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.51.0) gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
...@@ -1224,4 +1224,4 @@ DEPENDENCIES ...@@ -1224,4 +1224,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.15.4 1.16.0
...@@ -141,21 +141,29 @@ the stable branch are: ...@@ -141,21 +141,29 @@ the stable branch are:
* Fixes for security issues * Fixes for security issues
* New or updated translations (as long as they do not touch application code) * New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the upcoming During the feature freeze all merge requests that are meant to go into the
release should have the correct milestone assigned _and_ have the label upcoming release should have the correct milestone assigned _and_ the
~"Pick into Stable" set, so that release managers can find and pick them. `Pick into X.Y` label where `X.Y` is equal to the milestone, so that release
Merge requests without a milestone and this label will managers can find and pick them.
not be merged into any stable branches. Merge requests without this label will not be picked into the stable release.
Fixes marked like this will be shipped in the next RC for that release. Once For example, if the upcoming release is `10.2.0` you will need to set the
the final RC has been prepared ready for release on the 22nd, further fixes `Pick into 10.2` label.
marked ~"Pick into Stable" will go into a patch for that release.
Fixes marked like this will be shipped in the next RC (before the 22nd), or the
If a merge request is to be picked into more than one release it will also need next patch release.
the ~"Pick into Backports" label set to remind the release manager to change
the milestone after cherry-picking. As before, it should still have the If a merge request is to be picked into more than one release it will need one
~"Pick into Stable" label and the milestone of the highest release it will be `Pick into X.Y` label per release where the merge request should be back-ported
picked into. to.
For example, if the current patch release is `10.1.1` and a regression fix needs
to be backported down to the `9.5` release, you will need to assign it the
`10.1` milestone and the following labels:
- `Pick into 10.1`
- `Pick into 10.0`
- `Pick into 9.5`
### Asking for an exception ### Asking for an exception
......
{"iconCount":173,"spriteSize":75815,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} {"iconCount":174,"spriteSize":76324,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file \ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="b" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="c" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="d" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="e" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="f" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="rotate(5 202.071 210.085)" rx="10"/><g transform="rotate(15 -104.714 891.23)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#d2caea" fill-rule="nonzero" d="M96.153 81.151a2.001 2.001 0 0 0 2.184-.496l35.956-38.34a2 2 0 1 0-2.918-2.736l-35.03 37.36-41.888-16.285a2 2 0 0 0-2.16.471l-26.368 27.16a2 2 0 1 0 2.87 2.786l25.444-26.21 41.911 16.294"/><g fill="#fff" transform="translate(24.368 36.951)"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="rotate(-5 116.372 150.825)" rx="10"/><g transform="rotate(5 -1514.687 1518.752)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#e)" xlink:href="#b"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="M84.67 28.41c18.225 0 33 15.07 33 33.651h-33V28.41" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="M78.67 66.41h30a2 2 0 0 1 2 2c0 18.778-15.222 34-34 34s-34-15.222-34-34 15.222-34 34-34a2 2 0 0 1 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28H76.67a2 2 0 0 1-2-2V38.476c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="rotate(-5 1023.06 -299.524)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#f)" xlink:href="#c"/><path fill="#fef0ea" d="M42 47.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391V97H42V47.391"/><path fill="#fb722e" d="M108 55.406c0-.777.628-1.406 1.4-1.406h9.2a1.4 1.4 0 0 1 1.4 1.406V97h-12V55.406"/><path fill="#6b4fbb" d="M64 35.404c0-.776.628-1.404 1.4-1.404h9.2a1.4 1.4 0 0 1 1.4 1.404v61.6H64v-61.6"/><path fill="#d2caea" d="M86 73.4a1.4 1.4 0 0 1 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602H86V73.4"/></g><g fill="#fee8dc"><path d="M3.592 93.86l-2.454-1.562c-.93-.592-.924-1.554 0-2.143l2.454-1.562 1.562-2.454c.592-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143L8.86 93.86l-1.562 2.454c-.591.93-1.554.924-2.143 0L3.592 93.86M309.489 52.07l-3.14-1.998c-1.12-.713-1.128-1.863 0-2.581l3.14-2 1.999-3.14c.713-1.12 1.863-1.127 2.58 0l2 3.14 3.14 2c1.12.713 1.128 1.863 0 2.58l-3.14 2-2 3.14c-.712 1.12-1.862 1.128-2.58 0l-1.999-3.14"/></g><path fill="#e1dcf1" d="M128.073 11.066l-1.99 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/><path fill="#d2caea" d="M378.07 243.068l-1.989 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/></g></svg>
\ No newline at end of file \ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4a12.996 12.996 0 0 0-18.76 0h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/><path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/><path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15a4.012 4.012 0 0 1-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87a4.011 4.011 0 0 1-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="330" height="132" viewBox="0 0 330 132"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M174.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M211 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S230.33 4 211 4s-35 15.67-35 35 15.67 35 35 35z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M211.5 51c-6.42 0-12.26-2.84-17.43-8.4a4.008 4.008 0 0 1-.27-5.13C199 30.57 204.92 27 211.5 27s12.5 3.56 17.7 10.47a3.994 3.994 0 0 1-.27 5.12c-5.17 5.53-11 8.4-17.43 8.4zm0-4c5.25 0 10.05-2.34 14.5-7.13-4.5-5.98-9.3-8.87-14.5-8.87-5.2 0-10 2.9-14.5 8.87 4.45 4.8 9.25 7.13 14.5 7.13z"/><path fill="#FC6D26" d="M211 47c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-4c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></g><path fill="#000" fill-opacity=".03" d="M88.12 83c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M125 119c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M116 86.34c2.33.83 4 3.05 4 5.66 0 3.3-2.7 6-6 6s-6-2.7-6-6c0-2.6 1.67-4.83 4-5.66V72h4v14.34zM128 66c5.52 0 10 4.48 10 10v12h-4V76c0-3.3-2.7-6-6-6v1.83c0 .55-.45 1-1 1-.24 0-.47-.1-.65-.24l-4.46-3.87c-.46-.36-.5-1-.15-1.4.03-.05.07-.1.1-.12l4.47-3.82c.42-.35 1.05-.3 1.4.1.16.2.25.43.25.66V66zm-14 28c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#FC6D26" fill-rule="nonzero" d="M114 74c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm22 28c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#000" fill-opacity=".03" d="M2.12 52C2.04 53 2 54 2 55c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 71.03 58.42 86 39 86S3.65 71.03 2.12 52z"/><path fill="#EEE" fill-rule="nonzero" d="M39 88C17.46 88 0 70.54 0 49s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 14 39 14 4 29.67 4 49s15.67 35 35 35z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 41h-4c0-2.76-2.24-5-5-5s-5 2.24-5 5h-4a9 9 0 0 1 18 0zm-18 0h4v3h-4v-3zm14 0h4v3h-4v-3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 47c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V48c0-.55-.45-1-1-1H30zm0-4h18c2.76 0 5 2.24 5 5v12c0 2.76-2.24 5-5 5H30c-2.76 0-5-2.24-5-5V48c0-2.76 2.24-5 5-5z"/><path fill="#6B4FBB" d="M38 53.73c-.6-.34-1-1-1-1.73 0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.4-1 1.73V55c0 .55-.45 1-1 1s-1-.45-1-1v-1.27z"/><path fill="#000" fill-opacity=".03" d="M254.12 92c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M291 128c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#6B4BBE" fill-rule="nonzero" d="M292 78c5.52 0 10 4.48 10 10 0 2.28-.76 4.43-2.14 6.18-1.03 1.3-.8 3.2.5 4.22 1.3 1.02 3.2.8 4.2-.5 2.22-2.8 3.44-6.26 3.44-9.9 0-8.84-7.16-16-16-16v-3.13c0-.2-.06-.4-.17-.56-.3-.42-.93-.54-1.38-.23l-9.2 6.13c-.1.06-.2.16-.28.27-.3.45-.18 1.08.28 1.38l9.2 6.13c.16.1.35.17.55.17.55 0 1-.45 1-1V78z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M290 100c-5.52 0-10-4.48-10-10 0-2.25.74-4.38 2.1-6.12 1-1.3.77-3.2-.54-4.2-1.3-1.02-3.2-.78-4.2.53A15.796 15.796 0 0 0 274 90c0 8.84 7.16 16 16 16v3.13c0 .55.45 1 1 1 .2 0 .4-.06.55-.17l9.2-6.13c.46-.3.6-.93.28-1.38-.07-.1-.17-.2-.28-.28l-9.2-6.13c-.45-.3-1.08-.2-1.38.27-.1.2-.17.4-.17.6v3.1z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm1 5h10a2 2 0 1 1 0 4H34a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36a8.004 8.004 0 0 1 1.566-3.705c3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846a8.009 8.009 0 0 1 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1a3.997 3.997 0 0 0-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3a3.99 3.99 0 0 0-.784 1.853l-.346 2.36a4.003 4.003 0 0 1-3.942 3.42l-13.08.053a4 4 0 0 1-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268zm-6 0a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268z"/></g></svg>
\ No newline at end of file
import { truncate } from './lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500; const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
...@@ -15,7 +17,7 @@ export default class AbuseReports { ...@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) { if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage); $messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true'); $messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
} }
} }
......
<script>
import Flash from '~/flash';
import GitlabSlackService from '../services/gitlab_slack_service';
import * as UrlUtility from '../../lib/utils/url_utility';
export default {
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
gitlabForSlackGifPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
slackLinkPath: {
type: String,
required: true,
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
docsPath: {
type: String,
required: true,
},
},
data() {
return {
popupOpen: false,
selectedProjectId: this.projects && this.projects.length ? this.projects[0].id : 0,
};
},
computed: {
doubleHeadedArrowSvg() {
return gl.utils.spriteIcon('double-headed-arrow');
},
arrowRightSvg() {
return gl.utils.spriteIcon('arrow-right');
},
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => UrlUtility.redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
},
mounted() {
GitlabSlackService.init();
},
};
</script>
<template>
<div>
<div class="center append-right-default">
<h1>GitLab for Slack</h1>
<p>Track your GitLab projects with GitLab for Slack.</p>
</div>
<div class="append-bottom-20 center" v-once>
<img
class="gitlab-slack-logo"
:src="gitlabLogoPath"></img>
<div
class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20"
v-html="doubleHeadedArrowSvg"></div>
<img
class="gitlab-slack-logo"
:src="slackLogoPath"></img>
</div>
<button
type="button"
class="btn btn-red center-block js-popup-button"
@click="togglePopup">
Add GitLab to Slack
</button>
<div
class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup"
v-if="popupOpen">
<div
class="inline"
v-if="isSignedIn && hasProjects">
<strong>Select GitLab project to link with your Slack team</strong>
<select
class="gitlab-slack-project-select js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId">
<option
v-for="project in projects"
:key="project.id"
:value="project.id">
{{ project.name }}
</option>
</select>
<button
type="button"
class="btn btn-red pull-right js-add-button"
@click="addToSlack">
Add to Slack
</button>
</div>
<span
class="js-no-projects"
v-else-if="isSignedIn && !hasProjects">
You don't have any projects available.
</span>
<span v-else>
You have to
<a
class="js-gitlab-slack-sign-in-link"
v-once
:href="signInPath">
log in
</a>
</span>
</div>
<div class="center prepend-top-20 append-bottom-10 append-right-5 prepend-left-5">
<img
v-once
class="gitlab-slack-gif"
:src="gitlabForSlackGifPath">
</div>
<div
class="gitlab-slack-example"
v-once>
<h3 class="center">How it works</h3>
<div class="well gitlab-slack-well center-block">
<code class="code center-block append-bottom-10">/project-name issue show &lt;id&gt;</code>
<span>
<div
class="gitlab-slack-right-arrow inline append-right-5"
v-html="arrowRightSvg"></div>
Shows the issue with id
<strong>&lt;id&gt;</strong>
</span>
</div>
<div class="center">
<a :href="docsPath">
More Slack commands
</a>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import AddGitlabSlackApplication from './components/add_gitlab_slack_application.vue';
function mountAddGitlabSlackApplication() {
const el = document.getElementById('js-add-gitlab-slack-application-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-add-gitlab-slack-application-entry-data');
const initialData = JSON.parse(dataNode.innerHTML);
const AddGitlabSlackApplicationComp = Vue.extend(AddGitlabSlackApplication);
new AddGitlabSlackApplicationComp({
propsData: {
projects: initialData.projects,
isSignedIn: initialData.is_signed_in,
gitlabForSlackGifPath: initialData.gitlab_for_slack_gif_path,
signInPath: initialData.sign_in_path,
slackLinkPath: initialData.slack_link_profile_slack_path,
gitlabLogoPath: initialData.gitlab_logo_path,
slackLogoPath: initialData.slack_logo_path,
docsPath: initialData.docs_path,
},
}).$mount(el);
}
document.addEventListener('DOMContentLoaded', mountAddGitlabSlackApplication);
export default mountAddGitlabSlackApplication;
import axios from 'axios';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
export default {
init() {
setAxiosCsrfToken();
},
addToSlack(url, projectId) {
return axios.get(url, {
params: {
project_id: projectId,
},
});
},
};
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props, no-new */ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props, no-new */
/* global ProjectSelect */
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import groupsSelect from './groups_select'; import groupsSelect from './groups_select';
import './project_select'; import projectSelect from './project_select';
class AuditLogs { class AuditLogs {
constructor() { constructor() {
...@@ -11,7 +10,7 @@ class AuditLogs { ...@@ -11,7 +10,7 @@ class AuditLogs {
} }
initFilters() { initFilters() {
new ProjectSelect(); projectSelect();
groupsSelect(); groupsSelect();
new UsersSelect(); new UsersSelect();
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import './lists_dropdown'; import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() { submitText() {
const count = ModalStore.selectedCount(); const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
}, },
}, },
methods: { methods: {
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */ /* global Pager */
import { pluralize } from './lib/utils/text_utility';
export default (function () { export default (function () {
const CommitsList = {}; const CommitsList = {};
...@@ -86,7 +88,7 @@ export default (function () { ...@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header. // Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`); $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
} }
gl.utils.localTimeAgo($processedData.find('.js-timeago')); gl.utils.localTimeAgo($processedData.find('.js-timeago'));
......
/* eslint-disable func-names, prefer-arrow-callback */ /* eslint-disable func-names, prefer-arrow-callback */
import Api from './api'; import Api from './api';
import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown { export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) { constructor($el, namespacePath, projectPath) {
...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown { ...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message; errors = label.message;
} else { } else {
errors = Object.keys(label.message).map(key => errors = Object.keys(label.message).map(key =>
`${gl.text.humanize(key)} ${label.message[key].join(', ')}`, `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>'); ).join('<br/>');
} }
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { __ } from '../locale'; import { __ } from '../locale';
import '../lib/utils/text_utility'; import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
}); });
newData.stages.forEach((item) => { newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.name.toLowerCase()); const stageSlug = dasherize(item.name.toLowerCase());
item.active = false; item.active = false;
item.isUserAllowed = data.permissions[stageSlug]; item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import { s__ } from './locale'; import { s__ } from './locale';
/* global ProjectSelect */ import projectSelect from './project_select';
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
import IssuableForm from './issuable_form'; import IssuableForm from './issuable_form';
...@@ -27,8 +27,7 @@ import projectAvatar from './project_avatar'; ...@@ -27,8 +27,7 @@ import projectAvatar from './project_avatar';
/* global CompareAutocomplete */ /* global CompareAutocomplete */
/* global PathLocks */ /* global PathLocks */
/* global ProjectFindFile */ /* global ProjectFindFile */
/* global ProjectNew */ import ProjectNew from './project_new';
/* global ProjectShow */
import projectImport from './project_import'; import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
...@@ -95,6 +94,8 @@ import Members from './members'; ...@@ -95,6 +94,8 @@ import Members from './members';
import memberExpirationDate from './member_expiration_date'; import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select'; import DueDateSelectors from './due_date_select';
import Diff from './diff'; import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
import ProjectVariables from './project_variables';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -212,7 +213,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -212,7 +213,7 @@ import initGroupAnalytics from './init_group_analytics';
initIssuableSidebar(); initIssuableSidebar();
break; break;
case 'dashboard:milestones:index': case 'dashboard:milestones:index':
new ProjectSelect(); projectSelect();
break; break;
case 'projects:milestones:show': case 'projects:milestones:show':
new UserCallout(); new UserCallout();
...@@ -223,7 +224,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -223,7 +224,7 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'dashboard:issues': case 'dashboard:issues':
case 'dashboard:merge_requests': case 'dashboard:merge_requests':
new ProjectSelect(); projectSelect();
initLegacyFilters(); initLegacyFilters();
break; break;
case 'groups:issues': case 'groups:issues':
...@@ -232,7 +233,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -232,7 +233,7 @@ import initGroupAnalytics from './init_group_analytics';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
new ProjectSelect(); projectSelect();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new Todos(); new Todos();
...@@ -530,7 +531,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -530,7 +531,7 @@ import initGroupAnalytics from './init_group_analytics';
if ($el.find('.dropdown-group-label').length) { if ($el.find('.dropdown-group-label').length) {
new GroupLabelSubscription($el); new GroupLabelSubscription($el);
} else { } else {
new gl.ProjectLabelSubscription($el); new ProjectLabelSubscription($el);
} }
}); });
break; break;
...@@ -579,7 +580,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -579,7 +580,7 @@ import initGroupAnalytics from './init_group_analytics';
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
case 'groups:settings:ci_cd:show': case 'groups:settings:ci_cd:show':
new gl.ProjectVariables(); new ProjectVariables();
break; break;
case 'ci:lints:create': case 'ci:lints:create':
case 'ci:lints:show': case 'ci:lints:show':
...@@ -706,7 +707,6 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -706,7 +707,6 @@ import initGroupAnalytics from './init_group_analytics';
case 'show': case 'show':
new Star(); new Star();
new ProjectNew(); new ProjectNew();
new ProjectShow();
new NotificationsDropdown(); new NotificationsDropdown();
break; break;
case 'wikis': case 'wikis':
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility'; import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
if (this.hasManualActions) { if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => { return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = { const parsedAction = {
name: gl.text.humanize(action.name), name: humanize(action.name),
play_path: action.play_path, play_path: action.play_path,
playable: action.playable, playable: action.playable,
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import { parseSeconds, stringifyTime } from './lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from './lib/utils/pretty_time';
import { addDelimiter } from './lib/utils/text_utility';
const healthyClass = 'geo-node-healthy'; const healthyClass = 'geo-node-healthy';
const unhealthyClass = 'geo-node-unhealthy'; const unhealthyClass = 'geo-node-unhealthy';
...@@ -52,7 +53,7 @@ class GeoNodeStatus { ...@@ -52,7 +53,7 @@ class GeoNodeStatus {
static formatCountAndPercentage(count, total, percentage) { static formatCountAndPercentage(count, total, percentage) {
if (count !== null || total != null) { if (count !== null || total != null) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`; return `${addDelimiter(count)}/${addDelimiter(total)} (${percentage})`;
} }
return notAvailable; return notAvailable;
...@@ -60,7 +61,7 @@ class GeoNodeStatus { ...@@ -60,7 +61,7 @@ class GeoNodeStatus {
static formatCount(count) { static formatCount(count) {
if (count !== null) { if (count !== null) {
return gl.text.addDelimiter(count); return addDelimiter(count);
} }
return notAvailable; return notAvailable;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input'; import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
export default class GLForm { export default class GLForm {
constructor(form, enableGFM = false) { constructor(form, enableGFM = false) {
...@@ -46,7 +47,7 @@ export default class GLForm { ...@@ -46,7 +47,7 @@ export default class GLForm {
} }
// form and textarea event listeners // form and textarea event listeners
this.addEventListeners(); this.addEventListeners();
gl.text.init(this.form); textUtils.init(this.form);
// hide discard button // hide discard button
this.form.find('.js-note-discard').hide(); this.form.find('.js-note-discard').hide();
this.form.show(); this.form.show();
...@@ -85,7 +86,7 @@ export default class GLForm { ...@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() { clearEventListeners() {
this.textarea.off('focus'); this.textarea.off('focus');
this.textarea.off('blur'); this.textarea.off('blur');
gl.text.removeListeners(this.form); textUtils.removeListeners(this.form);
} }
addEventListeners() { addEventListeners() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility'; import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash'; import Flash from './flash';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
...@@ -73,7 +73,7 @@ export default class Issue { ...@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) { if (this.createMergeRequestDropdown) {
if (isClosed) { if (isClosed) {
......
...@@ -14,8 +14,8 @@ export default class Job { ...@@ -14,8 +14,8 @@ export default class Job {
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.$window = $(window);
this.logBytes = 0; this.logBytes = 0;
this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this); this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
...@@ -54,23 +54,18 @@ export default class Job { ...@@ -54,23 +54,18 @@ export default class Job {
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
$(window) this.$window
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
const contentHeight = this.$buildTraceOutput.height(); if (!this.isScrolledToBottom()) {
if (contentHeight > this.windowSize) {
// means the user did not scroll, the content was updated.
this.windowSize = contentHeight;
} else {
// User scrolled
this.hasBeenScrolled = true;
this.toggleScrollAnimation(false); this.toggleScrollAnimation(false);
} else if (this.isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
} }
this.scrollThrottled(); this.scrollThrottled();
}); });
$(window) this.$window
.off('resize.build') .off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
...@@ -99,14 +94,14 @@ export default class Job { ...@@ -99,14 +94,14 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
canScroll() { canScroll() {
return $(document).height() > $(window).height(); return this.$document.height() > this.$window.height();
} }
toggleScroll() { toggleScroll() {
const currentPosition = $(document).scrollTop(); const currentPosition = this.$document.scrollTop();
const scrollHeight = $(document).height(); const scrollHeight = this.$document.height();
const windowHeight = $(window).height(); const windowHeight = this.$window.height();
if (this.canScroll()) { if (this.canScroll()) {
if (currentPosition > 0 && if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) { (scrollHeight - currentPosition !== windowHeight)) {
...@@ -119,7 +114,7 @@ export default class Job { ...@@ -119,7 +114,7 @@ export default class Job {
this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (scrollHeight - currentPosition === windowHeight) { } else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log. // User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollTopBtn, false);
...@@ -131,9 +126,17 @@ export default class Job { ...@@ -131,9 +126,17 @@ export default class Job {
} }
} }
isScrolledToBottom() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const windowHeight = this.$window.height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
scrollDown() { scrollDown() {
$(document).scrollTop($(document).height()); this.$document.scrollTop(this.$document.height());
} }
scrollToBottom() { scrollToBottom() {
...@@ -143,7 +146,7 @@ export default class Job { ...@@ -143,7 +146,7 @@ export default class Job {
} }
scrollToTop() { scrollToTop() {
$(document).scrollTop(0); this.$document.scrollTop(0);
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.toggleScroll(); this.toggleScroll();
} }
...@@ -174,7 +177,7 @@ export default class Job { ...@@ -174,7 +177,7 @@ export default class Job {
this.state = log.state; this.state = log.state;
} }
this.windowSize = this.$buildTraceOutput.height(); this.isScrollInBottom = this.isScrolledToBottom();
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
...@@ -194,14 +197,9 @@ export default class Job { ...@@ -194,14 +197,9 @@ export default class Job {
} else { } else {
this.$truncatedInfo.addClass('hidden'); this.$truncatedInfo.addClass('hidden');
} }
this.isLogComplete = log.complete;
if (!log.complete) { if (!log.complete) {
if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.getBuildTrace(); this.getBuildTrace();
}, 4000); }, 4000);
...@@ -218,7 +216,7 @@ export default class Job { ...@@ -218,7 +216,7 @@ export default class Job {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}) })
.then(() => { .then(() => {
if (!this.hasBeenScrolled) { if (this.isScrollInBottom) {
this.scrollDown(); this.scrollDown();
} }
}) })
......
...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => { ...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment; return documentFragment;
}; };
// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => { export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart; const selectionStart = target.selectionStart;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import timeago from 'timeago.js'; import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format'; import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
import { import {
lang, lang,
...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = ''; let text = '';
if (minutes >= 1) { if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else { } else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${seconds} ${pluralize('second', seconds)}`;
} }
return text; return text;
} }
......
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
const textUtils = {};
textUtils.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
textUtils.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
textUtils.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
textUtils.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
textUtils.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
textUtils.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
export default textUtils;
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ /**
* Adds a , to a string composed by numbers, at every 3 chars.
import 'vendor/latinise'; *
* 2333 -> 2,333
var base; * 232324 -> 232,324
var w = window; *
if (w.gl == null) { * @param {String} text
w.gl = {}; * @returns {String}
} */
if ((base = w.gl).text == null) { export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
base.text = {};
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
/** /**
* Returns '99+' for numbers bigger than 99. * Returns '99+' for numbers bigger than 99.
...@@ -20,182 +15,50 @@ gl.text.addDelimiter = function(text) { ...@@ -20,182 +15,50 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count * @param {Number} count
* @return {Number|String} * @return {Number|String}
*/ */
export function highCountTrim(count) { export const highCountTrim = count => (count > 99 ? '99+' : count);
return count > 99 ? '99+' : count;
}
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
// re-use canvas object for better performance
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
gl.text.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
gl.text.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline /**
if (selected.indexOf('\n') === 0) { * Converst first char to uppercase and replaces undercores with spaces
removedFirstNewLine = true; * @param {String} string
selected = selected.replace(/\n+/, ''); * @requires {String}
} */
export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) { /**
insertText = '\n' + insertText; * Adds an 's' to the end of the string when count is bigger than 0
} * @param {String} str
* @param {Number} count
* @returns {String}
*/
export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
if (removedLastNewLine) { /**
insertText += '\n'; * Replaces underscores with dashes
} * @param {*} str
* @returns {String}
*/
export const dasherize = str => str.replace(/[_\s]+/g, '-');
if (document.queryCommandSupported('insertText')) { /**
inserted = document.execCommand('insertText', false, insertText); * Removes accents and converts to lower case
} * @param {String} str
if (!inserted) { * @returns {String}
try { */
document.execCommand("ms-beginUndoUnit"); export const slugify = str => str.trim().toLowerCase();
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) { /**
pos -= 1; * Truncates given text
} *
* @param {String} string
* @param {Number} maxLength
* @returns {String}
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
return textArea.setSelectionRange(pos, pos); /**
} * Capitalizes first character.
}; *
gl.text.updateText = function(textArea, tag, blockTag, wrap) { * @param {String} text
var $textArea, selected, text; * @returns {String}
$textArea = $(textArea); */
textArea = $textArea.get(0); export const capitalizeFirstCharacter = text => `${text[0].toUpperCase()}${text.slice(1)}`;
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
gl.text.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
gl.text.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
gl.text.dasherize = function(str) {
return str.replace(/[_\s]+/g, '-');
};
gl.text.slugify = function(str) {
return str.trim().toLowerCase().latinise();
};
...@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) { ...@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
} }
} }
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -30,7 +30,6 @@ import './commit/image_file'; ...@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/url_utility'; import './lib/utils/url_utility';
// behaviors // behaviors
...@@ -74,11 +73,6 @@ import './pager'; ...@@ -74,11 +73,6 @@ import './pager';
import './preview_markdown'; import './preview_markdown';
import './project_find_file'; import './project_find_file';
import './project_import'; import './project_import';
import './project_label_subscription';
import './project_new';
import './project_select';
import './project_show';
import './project_variables';
import './projects_dropdown'; import './projects_dropdown';
import './projects_list'; import './projects_list';
import './syntax_highlight'; import './syntax_highlight';
......
...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages'; ...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list'; import TaskList from './task_list';
import './merge_request_tabs'; import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
(function() { (function() {
this.MergeRequest = (function() { this.MergeRequest = (function() {
...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter'); const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
$el.text(gl.text.addDelimiter(count)); $el.text(addDelimiter(count));
}; };
MergeRequest.prototype.hideCloseButton = function() { MergeRequest.prototype.hideCloseButton = function() {
......
...@@ -357,7 +357,8 @@ ...@@ -357,7 +357,8 @@
@click="handleSave(true)" @click="handleSave(true)"
v-if="canUpdateIssue" v-if="canUpdateIssue"
:class="actionButtonClassNames" :class="actionButtonClassNames"
class="btn btn-comment btn-comment-and-close"> :disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}} {{issueActionButtonTitle}}
</button> </button>
<button <button
......
<script> <script>
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
import { dasherize } from '../../../lib/utils/text_utility';
/** /**
* Renders either a cancel, retry or play icon pointing to the given path. * Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead. * TODO: Remove UJS from here and use an async request instead.
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = gl.text.dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import projectSelect from './project_select';
export default class Project { export default class Project {
constructor() { constructor() {
...@@ -58,7 +58,7 @@ export default class Project { ...@@ -58,7 +58,7 @@ export default class Project {
} }
static projectSelectDropdown() { static projectSelectDropdown() {
new ProjectSelect(); projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
} }
......
/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
(function(global) { this.$buttons.on('click', this.toggleSubscription.bind(this));
class ProjectLabelSubscription { }
constructor(container) {
this.$container = $(container);
this.$buttons = this.$container.find('.js-subscribe-button');
this.$buttons.on('click', this.toggleSubscription.bind(this));
}
toggleSubscription(event) { toggleSubscription(event) {
event.preventDefault(); event.preventDefault();
const $btn = $(event.currentTarget); const $btn = $(event.currentTarget);
const $span = $btn.find('span'); const $span = $btn.find('span');
const url = $btn.attr('data-url'); const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status'); const oldStatus = $btn.attr('data-status');
$btn.addClass('disabled'); $btn.addClass('disabled');
$span.toggleClass('hidden'); $span.toggleClass('hidden');
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url url,
}).done(() => { }).done(() => {
let newStatus, newAction; let newStatus;
let newAction;
if (oldStatus === 'unsubscribed') { if (oldStatus === 'unsubscribed') {
[newStatus, newAction] = ['subscribed', 'Unsubscribe']; [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
} else { } else {
[newStatus, newAction] = ['unsubscribed', 'Subscribe']; [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
} }
$span.toggleClass('hidden'); $span.toggleClass('hidden');
$btn.removeClass('disabled'); $btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus); this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction); this.$buttons.find('> span').text(newAction);
this.$buttons.map((button) => { this.$buttons.map((button) => {
const $button = $(button); const $button = $(button);
if ($button.attr('data-original-title')) { if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
} }
return button; return button;
});
}); });
} });
} }
}
global.ProjectLabelSubscription = ProjectLabelSubscription;
})(window.gl || (window.gl = {}));
This diff is collapsed.
...@@ -2,79 +2,73 @@ ...@@ -2,79 +2,73 @@
import Api from './api'; import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button'; import ProjectSelectComboButton from './project_select_combo_button';
(function () { export default function projectSelect() {
this.ProjectSelect = (function () { $('.ajax-project-select').each(function(i, select) {
function ProjectSelect() { var placeholder;
$('.ajax-project-select').each(function(i, select) { const simpleFilter = $(select).data('simple-filter') || false;
var placeholder; this.groupId = $(select).data('group-id');
const simpleFilter = $(select).data('simple-filter') || false; this.includeGroups = $(select).data('include-groups');
this.groupId = $(select).data('group-id'); this.allProjects = $(select).data('all-projects') || false;
this.includeGroups = $(select).data('include-groups'); this.orderBy = $(select).data('order-by') || 'id';
this.allProjects = $(select).data('all-projects') || false; this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.orderBy = $(select).data('order-by') || 'id'; this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
placeholder = "Search for project"; placeholder = "Search for project";
if (this.includeGroups) { if (this.includeGroups) {
placeholder += " or group"; placeholder += " or group";
} }
$(select).select2({ $(select).select2({
placeholder: placeholder, placeholder: placeholder,
minimumInputLength: 0, minimumInputLength: 0,
query: (function (_this) { query: (function (_this) {
return function (query) { return function (query) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
finalCallback = function (projects) { finalCallback = function (projects) {
var data;
data = {
results: projects
};
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data; var data;
data = { data = groups.concat(projects);
results: projects return finalCallback(data);
};
return query.callback(data);
}; };
if (_this.includeGroups) { return Api.groups(query.term, {}, groupsCallback);
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (_this.groupId) {
return Api.groupProjects(_this.groupId, query.term, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
}; };
})(this), } else {
id: function(project) { projectsCallback = finalCallback;
if (simpleFilter) return project.id; }
return JSON.stringify({ if (_this.groupId) {
name: project.name, return Api.groupProjects(_this.groupId, query.term, projectsCallback);
url: project.web_url, } else {
}); return Api.projects(query.term, {
}, order_by: _this.orderBy,
text: function (project) { with_issues_enabled: _this.withIssuesEnabled,
return project.name_with_namespace || project.name; with_merge_requests_enabled: _this.withMergeRequestsEnabled,
}, membership: !_this.allProjects,
dropdownCssClass: "ajax-project-dropdown" }, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
}); });
if (simpleFilter) return select; },
return new ProjectSelectComboButton(select); text: function (project) {
}); return project.name_with_namespace || project.name;
} },
dropdownCssClass: "ajax-project-dropdown"
return ProjectSelect; });
})(); if (simpleFilter) return select;
}).call(window); return new ProjectSelectComboButton(select);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
})();
}).call(window);
// I kept class for future
(() => {
const HIDDEN_VALUE_TEXT = '******';
class ProjectVariables { const HIDDEN_VALUE_TEXT = '******';
constructor() {
this.$revealBtn = $('.js-btn-toggle-reveal-values'); export default class ProjectVariables {
this.$revealBtn.on('click', this.toggleRevealState.bind(this)); constructor() {
} this.$revealBtn = $('.js-btn-toggle-reveal-values');
this.$revealBtn.on('click', this.toggleRevealState.bind(this));
}
toggleRevealState(e) { toggleRevealState(e) {
e.preventDefault(); e.preventDefault();
const oldStatus = this.$revealBtn.attr('data-status'); const oldStatus = this.$revealBtn.attr('data-status');
let newStatus = 'hidden'; let newStatus = 'hidden';
let newAction = 'Reveal Values'; let newAction = 'Reveal Values';
if (oldStatus === 'hidden') { if (oldStatus === 'hidden') {
newStatus = 'revealed'; newStatus = 'revealed';
newAction = 'Hide Values'; newAction = 'Hide Values';
} }
this.$revealBtn.attr('data-status', newStatus); this.$revealBtn.attr('data-status', newStatus);
const $variables = $('.variable-value'); const $variables = $('.variable-value');
$variables.each((_, variable) => { $variables.each((_, variable) => {
const $variable = $(variable); const $variable = $(variable);
let newText = HIDDEN_VALUE_TEXT; let newText = HIDDEN_VALUE_TEXT;
if (newStatus === 'revealed') { if (newStatus === 'revealed') {
newText = $variable.attr('data-value'); newText = $variable.attr('data-value');
} }
$variable.text(newText); $variable.text(newText);
}); });
this.$revealBtn.text(newAction); this.$revealBtn.text(newAction);
}
} }
}
window.gl = window.gl || {};
window.gl.ProjectVariables = ProjectVariables;
})();
...@@ -3,7 +3,7 @@ import flash from '../../flash'; ...@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const redirectToUrl = url => gl.utils.visitUrl(url); export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n ...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) { if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else { } else {
commit(types.SET_COMMIT_REF, data.id); commit(types.SET_COMMIT_REF, data.id);
......
...@@ -3,16 +3,16 @@ import * as types from '../mutation_types'; ...@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils'; import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch( export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
rootState.project.id, state.project.id,
{ {
branch, branch,
ref: rootState.currentBranch, ref: state.currentBranch,
}, },
).then(res => res.json()) ).then(res => res.json())
.then((data) => { .then((data) => {
const branchName = data.name; const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName); const url = location.href.replace(state.currentBranch, branchName);
pushState(url); pushState(url);
......
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
}, },
commitsText() { commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount); return pluralize('commit', this.mr.divergedCommitsCount);
}, },
branchNameClipboardData() { branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that // This supports code in app/assets/javascripts/copy_to_clipboard.js that
......
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return this.mr.relatedLinks; return !!this.mr.relatedLinks;
}, },
shouldRenderDeployments() { shouldRenderDeployments() {
return this.mr.deployments.length; return this.mr.deployments.length;
......
import bp from './breakpoints'; import bp from './breakpoints';
import { slugify } from './lib/utils/text_utility';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
...@@ -23,7 +24,7 @@ export default class Wikis { ...@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return; if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const slug = gl.text.slugify(slugInput.value); const slug = slugify(slugInput.value);
if (slug.length > 0) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path'); const wikisPath = slugInput.getAttribute('data-wikis-path');
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
@import "framework/modal"; @import "framework/modal";
@import "framework/pagination"; @import "framework/pagination";
@import "framework/panels"; @import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements"; @import "framework/secondary-navigation-elements";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
......
...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
} }
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
padding-right: $gl-padding-top;
}
.form-control {
width: 100%;
}
.btn-save {
display: flex;
margin-top: $gl-btn-padding;
}
}
.empty-state .new-epic-dropdown {
display: inline-flex;
.btn-save {
margin-left: 0;
margin-bottom: 0;
}
.btn-new {
margin: 0;
}
}
...@@ -401,10 +401,13 @@ ...@@ -401,10 +401,13 @@
.breadcrumbs-list { .breadcrumbs-list {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
flex-wrap: wrap;
margin-bottom: 0; margin-bottom: 0;
line-height: 16px; line-height: 16px;
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
> li { > li {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -412,24 +415,35 @@ ...@@ -412,24 +415,35 @@
padding: 2px 0; padding: 2px 0;
&:not(:last-child) { &:not(:last-child) {
margin-right: 20px; padding-right: 20px;
&:not(.dropdown) {
overflow: hidden;
}
} }
> a { > a {
font-size: 12px; font-size: 12px;
color: currentColor; color: currentColor;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 0 1 auto;
} }
} }
} }
.breadcrumb-item-text { .breadcrumb-item-text {
@include str-truncated(128px);
text-decoration: inherit; text-decoration: inherit;
@media (max-width: $screen-xs-max) {
@include str-truncated(128px);
}
} }
.breadcrumbs-list-angle { .breadcrumbs-list-angle {
position: absolute; position: absolute;
right: -12px; right: 7px;
top: 50%; top: 50%;
color: $gl-text-color-tertiary; color: $gl-text-color-tertiary;
transform: translateY(-50%); transform: translateY(-50%);
......
...@@ -216,3 +216,31 @@ ...@@ -216,3 +216,31 @@
display: none; display: none;
} }
} }
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
}
}
.popup {
@include triangle(
$gray-lighter,
$gray-darker,
$popup-triangle-size,
$popup-triangle-border-size
);
padding: $gl-padding;
background-color: $gray-lighter;
border: 1px solid $gray-darker;
border-radius: $border-radius-default;
box-shadow: 0 5px 8px $popup-box-shadow-color;
position: relative;
}
...@@ -741,3 +741,21 @@ Image Commenting cursor ...@@ -741,3 +741,21 @@ Image Commenting cursor
*/ */
$image-comment-cursor-left-offset: 12; $image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30; $image-comment-cursor-top-offset: 30;
/*
Add GitLab Slack Application
*/
$add-to-slack-popup-max-width: 400px;
$add-to-slack-gif-max-width: 850px;
$add-to-slack-well-max-width: 750px;
$add-to-slack-logo-size: 100px;
$double-headed-arrow-width: 100px;
$double-headed-arrow-height: 25px;
$right-arrow-size: 16px;
/*
Popup
*/
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
...@@ -420,3 +420,41 @@ table.u2f-registrations { ...@@ -420,3 +420,41 @@ table.u2f-registrations {
} }
} }
} }
.gitlab-slack-gif {
width: 100%;
max-width: $add-to-slack-gif-max-width;
}
.gitlab-slack-well {
background-color: $white-light;
box-shadow: none;
max-width: $add-to-slack-well-max-width;
}
.gitlab-slack-logo {
width: $add-to-slack-logo-size;
height: $add-to-slack-logo-size;
}
.gitlab-slack-popup {
width: 100%;
max-width: $add-to-slack-popup-max-width;
}
.gitlab-slack-right-arrow svg {
fill: $white-dark;
width: $right-arrow-size;
height: $right-arrow-size;
vertical-align: text-bottom;
}
.gitlab-slack-double-headed-arrow {
vertical-align: text-top;
svg {
fill: $gray-darker;
width: $double-headed-arrow-width;
height: $double-headed-arrow-height;
}
}
...@@ -274,3 +274,22 @@ ...@@ -274,3 +274,22 @@
} }
} }
} }
.modal-doorkeepr-auth,
.doorkeeper-app-form {
.scope-description {
color: $theme-gray-700;
}
}
.modal-doorkeepr-auth {
.modal-body {
padding: $gl-padding;
}
}
.doorkeeper-app-form {
.scope-description {
margin: 0 0 5px 17px;
}
}
...@@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private private
def set_application_setting def set_application_setting
@application_setting = current_application_settings @application_setting = ApplicationSetting.current
end end
def application_setting_params def application_setting_params
......
class Admin::GroupsController < Admin::ApplicationController class Admin::GroupsController < Admin::ApplicationController
prepend EE::Admin::GroupsController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
...@@ -68,10 +70,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -68,10 +70,10 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(group_params_ce << group_params_ee) params.require(:group).permit(allowed_group_params)
end end
def group_params_ce def allowed_group_params
[ [
:avatar, :avatar,
:description, :description,
...@@ -84,12 +86,4 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -84,12 +86,4 @@ class Admin::GroupsController < Admin::ApplicationController
:two_factor_grace_period :two_factor_grace_period
] ]
end end
def group_params_ee
[
:repository_size_limit,
:shared_runners_minutes_limit,
:plan_id
]
end
end end
class Admin::UsersController < Admin::ApplicationController class Admin::UsersController < Admin::ApplicationController
prepend EE::Admin::UsersController
before_action :user, except: [:index, :new, :create] before_action :user, except: [:index, :new, :create]
def index def index
...@@ -187,10 +189,10 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -187,10 +189,10 @@ class Admin::UsersController < Admin::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(user_params_ce << user_params_ee) params.require(:user).permit(allowed_user_params)
end end
def user_params_ce def allowed_user_params
[ [
:access_level, :access_level,
:avatar, :avatar,
...@@ -218,13 +220,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -218,13 +220,6 @@ class Admin::UsersController < Admin::ApplicationController
] ]
end end
def user_params_ee
[
:note,
namespace_attributes: [:id, :shared_runners_minutes_limit, :plan_id]
]
end
def update_user(&block) def update_user(&block)
result = Users::UpdateService.new(current_user, user: user).execute(&block) result = Users::UpdateService.new(current_user, user: user).execute(&block)
......
...@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :authenticate_user_from_personal_access_token! before_action :authenticate_sessionless_user!
before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
...@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base ...@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user) return try(:authenticated_user)
end end
def authenticate_user_from_personal_access_token! # This filter handles personal access tokens, and atom requests with rss tokens
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence def authenticate_sessionless_user!
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
return unless token.present? sessionless_sign_in(user) if user
user = User.find_by_personal_access_token(token)
sessionless_sign_in(user)
end
# This filter handles authentication for atom request with an rss_token
def authenticate_user_from_rss_token!
return unless request.format.atom?
token = params[:rss_token].presence
return unless token.present?
user = User.find_by_rss_token(token)
sessionless_sign_in(user)
end end
def verify_namespace_plan_check_enabled def verify_namespace_plan_check_enabled
......
...@@ -11,6 +11,7 @@ module LfsRequest ...@@ -11,6 +11,7 @@ module LfsRequest
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
prepend EE::LfsRequest
before_action :require_lfs_enabled! before_action :require_lfs_enabled!
before_action :lfs_check_access! before_action :lfs_check_access!
end end
...@@ -111,40 +112,4 @@ module LfsRequest ...@@ -111,40 +112,4 @@ module LfsRequest
def has_authentication_ability?(capability) def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability) (authentication_abilities || []).include?(capability)
end end
module EE
def lfs_forbidden!
raise NotImplementedError unless defined?(super)
if project.above_size_limit? || objects_exceed_repo_limit?
render_size_error
else
super
end
end
def render_size_error
render(
json: {
message: Gitlab::RepositorySizeError.new(project).push_error(@exceeded_limit),
documentation_url: help_url
},
content_type: "application/vnd.git-lfs+json",
status: 406
)
end
def objects_exceed_repo_limit?
return false unless project.size_limit_enabled?
return @limit_exceeded if defined?(@limit_exceeded)
lfs_push_size = objects.sum { |o| o[:size] }
size_with_lfs_push = project.repository_and_lfs_size + lfs_push_size
@exceeded_limit = size_with_lfs_push - project.actual_size_limit
@limit_exceeded = @exceeded_limit > 0
end
end
prepend EE
end end
module ServiceParams module ServiceParams
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepend EE::ServiceParams
ALLOWED_PARAMS_CE = [ ALLOWED_PARAMS_CE = [
:active, :active,
...@@ -62,19 +63,12 @@ module ServiceParams ...@@ -62,19 +63,12 @@ module ServiceParams
:webhook :webhook
].freeze ].freeze
ALLOWED_PARAMS_EE = [
:jenkins_url,
:multiproject_enabled,
:pass_unstable,
:project_name
].freeze
# Parameters to ignore if no value is specified # Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password].freeze FILTER_BLANK_PARAMS = [:password].freeze
def service_params def service_params
dynamic_params = @service.event_channel_names + @service.event_names dynamic_params = @service.event_channel_names + @service.event_names
service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + ALLOWED_PARAMS_EE + dynamic_params) service_params = params.permit(:id, service: allowed_service_params + dynamic_params)
if service_params[:service].is_a?(Hash) if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param| FILTER_BLANK_PARAMS.each do |param|
...@@ -84,4 +78,8 @@ module ServiceParams ...@@ -84,4 +78,8 @@ module ServiceParams
service_params service_params
end end
def allowed_service_params
ALLOWED_PARAMS_CE
end
end end
...@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def redirect_out_of_range(todos) def redirect_out_of_range(todos)
total_pages = total_pages =
if todo_params.except(:sort, :page).empty? if todo_params.except(:sort, :page).empty?
(current_user.todos_pending_count / todos.limit_value).ceil (current_user.todos_pending_count.to_f / todos.limit_value).ceil
else else
todos.total_pages todos.total_pages
end end
......
...@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user if current_user
log_audit_event(current_user, with: :saml) log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed. # Update SAML identity if data has changed.
identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil? if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
...@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth def handle_omniauth
if current_user if current_user
# Add new authentication method # Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
else else
......
...@@ -74,7 +74,11 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -74,7 +74,11 @@ class Projects::WikisController < Projects::ApplicationController
def history def history
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
unless @page if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
total_count: @page.count_versions)
.page(params[:page])
else
redirect_to( redirect_to(
project_wiki_path(@project, :home), project_wiki_path(@project, :home),
notice: "Page not found" notice: "Page not found"
...@@ -102,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -102,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project) redirect_to project_path(@project)
......
module Geo
class ExpireUploadsFinder
def find_project_uploads(project)
if Gitlab::Geo.fdw?
fdw_find_project_uploads(project)
else
legacy_find_project_uploads(project)
end
end
def find_file_registries_uploads(project)
if Gitlab::Geo.fdw?
fdw_find_file_registries_uploads(project)
else
legacy_find_file_registries_uploads(project)
end
end
#
# FDW accessors
#
# @return [ActiveRecord::Relation<Geo::Fdw::Upload>]
def fdw_find_project_uploads(project)
fdw_table = Geo::Fdw::Upload.table_name
upload_type = 'file'
Geo::Fdw::Upload.joins("JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND #{fdw_table}.model_id='#{project.id}'
AND #{fdw_table}.model_type='#{project.class.name}'
AND file_registry.file_type='#{upload_type}'")
end
# @return [ActiveRecord::Relation<Geo::FileRegistry>]
def fdw_find_file_registries_uploads(project)
fdw_table = Geo::Fdw::Upload.table_name
upload_type = 'file'
Geo::FileRegistry.joins("JOIN #{fdw_table}
ON file_registry.file_id = #{fdw_table}.id
AND #{fdw_table}.model_id='#{project.id}'
AND #{fdw_table}.model_type='#{project.class.name}'
AND file_registry.file_type='#{upload_type}'")
end
#
# Legacy accessors (non FDW)
#
# @return [ActiveRecord::Relation<Geo::FileRegistry>] list of file registry items
def legacy_find_file_registries_uploads(project)
upload_ids = Upload.where(model_type: project.class.name, model_id: project.id).pluck(:id)
return Geo::FileRegistry.none if upload_ids.empty?
values_sql = upload_ids.map { |id| "(#{id})" }.join(',')
upload_type = 'file'
Geo::FileRegistry.joins(<<~SQL)
JOIN (VALUES #{values_sql})
AS uploads (id)
ON uploads.id = file_registry.file_id
AND file_registry.file_type='#{upload_type}'
SQL
end
# @return [ActiveRecord::Relation<Upload>] list of upload files
def legacy_find_project_uploads(project)
file_registry_ids = legacy_find_file_registries_uploads(project).pluck(:file_id)
return Upload.none if file_registry_ids.empty?
values_sql = file_registry_ids.map { |f_id| "(#{f_id})" }.join(',')
Upload.joins(<<~SQL)
JOIN (VALUES #{values_sql})
AS file_registry (file_id)
ON (file_registry.file_id = uploads.id)
SQL
end
end
end
module Geo
class RegistryFinder
attr_reader :current_node
def initialize(current_node: nil)
@current_node = current_node
end
def find_failed_objects(batch_size:)
Geo::FileRegistry
.failed
.retry_due
.limit(batch_size)
.pluck(:file_id, :file_type)
end
# Find limited amount of non replicated lfs objects.
#
# You can pass a list with `except_registry_ids:` so you can exclude items you
# already scheduled but haven't finished and persisted to the database yet
#
# TODO: Alternative here is to use some sort of window function with a cursor instead
# of simply limiting the query and passing a list of items we don't want
#
# @param [Integer] batch_size used to limit the results returned
# @param [Array<Integer>] except_registry_ids ids that will be ignored from the query
def find_nonreplicated_lfs_objects(batch_size:, except_registry_ids:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_nonreplicated_lfs_objects
else
legacy_find_nonreplicated_lfs_objects(except_registry_ids: except_registry_ids)
end
relation
.limit(batch_size)
.pluck(:id)
.map { |id| [id, :lfs] }
end
# Find limited amount of non replicated uploads.
#
# You can pass a list with `except_registry_ids:` so you can exclude items you
# already scheduled but haven't finished and persisted to the database yet
#
# TODO: Alternative here is to use some sort of window function with a cursor instead
# of simply limiting the query and passing a list of items we don't want
#
# @param [Integer] batch_size used to limit the results returned
# @param [Array<Integer>] except_registry_ids ids that will be ignored from the query
def find_nonreplicated_uploads(batch_size:, except_registry_ids:)
# Selective project replication adds a wrinkle to FDW queries, so
# we fallback to the legacy version for now.
relation =
if Gitlab::Geo.fdw? && !selective_sync
fdw_find_nonreplicated_uploads
else
legacy_find_nonreplicated_uploads(except_registry_ids: except_registry_ids)
end
relation
.limit(batch_size)
.pluck(:id, :uploader)
.map { |id, uploader| [id, uploader.sub(/Uploader\z/, '').underscore] }
end
protected
def selective_sync
current_node.restricted_project_ids
end
#
# FDW accessors
#
def fdw_find_nonreplicated_lfs_objects
fdw_table = Geo::Fdw::LfsObject.table_name
# Filter out objects in object storage (this is done in GeoNode#lfs_objects)
Geo::Fdw::LfsObject.joins("LEFT OUTER JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND file_registry.file_type = 'lfs'")
.where("#{fdw_table}.file_store IS NULL OR #{fdw_table}.file_store = #{LfsObjectUploader::LOCAL_STORE}")
.where('file_registry.file_id IS NULL')
end
def fdw_find_nonreplicated_uploads
fdw_table = Geo::Fdw::Upload.table_name
upload_types = Geo::FileService::DEFAULT_OBJECT_TYPES.map { |val| "'#{val}'" }.join(',')
Geo::Fdw::Upload.joins("LEFT OUTER JOIN file_registry
ON file_registry.file_id = #{fdw_table}.id
AND file_registry.file_type IN (#{upload_types})")
.where('file_registry.file_id IS NULL')
end
#
# Legacy accessors (non FDW)
#
def legacy_find_nonreplicated_lfs_objects(except_registry_ids:)
registry_ids = legacy_pluck_registry_ids(file_types: :lfs, except_registry_ids: except_registry_ids)
legacy_filter_registry_ids(
current_node.lfs_objects,
registry_ids,
LfsObject.table_name
)
end
def legacy_find_nonreplicated_uploads(except_registry_ids:)
registry_ids = legacy_pluck_registry_ids(file_types: Geo::FileService::DEFAULT_OBJECT_TYPES, except_registry_ids: except_registry_ids)
legacy_filter_registry_ids(
current_node.uploads,
registry_ids,
Upload.table_name
)
end
# This query requires data from two different databases, and unavoidably
# plucks a list of file IDs from one into the other. This will not scale
# well with the number of synchronized files--the query will increase
# linearly in size--so this should be replaced with postgres_fdw ASAP.
def legacy_filter_registry_ids(objects, registry_ids, table_name)
return objects if registry_ids.empty?
joined_relation = objects.joins(<<~SQL)
LEFT OUTER JOIN
(VALUES #{registry_ids.map { |id| "(#{id}, 't')" }.join(',')})
file_registry(file_id, registry_present)
ON #{table_name}.id = file_registry.file_id
SQL
joined_relation.where(file_registry: { registry_present: [nil, false] })
end
def legacy_pluck_registry_ids(file_types:, except_registry_ids:)
ids = Geo::FileRegistry.where(file_type: file_types).pluck(:file_id)
(ids + except_registry_ids).uniq
end
end
end
...@@ -232,6 +232,15 @@ module ApplicationSettingsHelper ...@@ -232,6 +232,15 @@ module ApplicationSettingsHelper
:sign_in_text, :sign_in_text,
:signup_enabled, :signup_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_period_in_seconds,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_requests_per_period,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_api_enabled,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period, :two_factor_grace_period,
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:unique_ips_limit_per_user, :unique_ips_limit_per_user,
......
...@@ -23,10 +23,17 @@ module IconsHelper ...@@ -23,10 +23,17 @@ module IconsHelper
render "shared/icons/#{icon_name}.svg", size: size render "shared/icons/#{icon_name}.svg", size: size
end end
def sprite_icon_path
# SVG Sprites currently don't work across domains, so in the case of a CDN
# we have to set the current path deliberately to prevent addition of asset_host
sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
end
def sprite_icon(icon_name, size: nil, css_class: nil) def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : "" css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank? css_classes << " #{css_class}" unless css_class.blank?
content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end end
def audit_icon(names, options = {}) def audit_icon(names, options = {})
......
module ServicesHelper module ServicesHelper
prepend EE::ServicesHelper
def service_event_description(event) def service_event_description(event)
case event case event
when "push", "push_events" when "push", "push_events"
......
...@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil, sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0, terminal_max_session_time: 0,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
......
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Importable include Importable
prepend EE::Build prepend EE::Ci::Build
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
...@@ -40,7 +40,6 @@ module Ci ...@@ -40,7 +40,6 @@ module Ci
scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, ArtifactUploader::LOCAL_STORE]) } scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, ArtifactUploader::LOCAL_STORE]) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :codequality, ->() { where(name: %w[codequality codeclimate]) }
scope :ref_protected, -> { where(protected: true) } scope :ref_protected, -> { where(protected: true) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
...@@ -471,11 +470,6 @@ module Ci ...@@ -471,11 +470,6 @@ module Ci
trace trace
end end
def has_codeclimate_json?
options.dig(:artifacts, :paths) == ['codeclimate.json'] &&
artifacts_metadata?
end
def serializable_hash(options = {}) def serializable_hash(options = {})
super(options).merge(when: read_attribute(:when)) super(options).merge(when: read_attribute(:when))
end end
......
...@@ -481,10 +481,6 @@ module Ci ...@@ -481,10 +481,6 @@ module Ci
.fabricate! .fabricate!
end end
def codeclimate_artifact
artifacts.codequality.find(&:has_codeclimate_json?)
end
def latest_builds_with_artifacts def latest_builds_with_artifacts
@latest_builds_with_artifacts ||= builds.latest.with_artifacts @latest_builds_with_artifacts ||= builds.latest.with_artifacts
end end
......
...@@ -4,15 +4,26 @@ module Avatarable ...@@ -4,15 +4,26 @@ module Avatarable
def avatar_path(only_path: true) def avatar_path(only_path: true)
return unless self[:avatar].present? return unless self[:avatar].present?
# If only_path is true then use the relative path of avatar.
# Otherwise use full path (including host).
asset_host = ActionController::Base.asset_host asset_host = ActionController::Base.asset_host
gitlab_host = only_path ? gitlab_config.relative_url_root : gitlab_config.url use_asset_host = asset_host.present?
# If asset_host is set then it is expected that assets are handled by a standalone host. # Avatars for private and internal groups and projects require authentication to be viewed,
# That means we do not want to get GitLab's relative_url_root option anymore. # which means they can only be served by Rails, on the regular GitLab host.
host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host # If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public?
use_asset_host = false
only_path = false
end
[host, avatar.url].join url_base = ""
if use_asset_host
url_base << asset_host unless only_path
else
url_base << gitlab_config.base_url unless only_path
url_base << gitlab_config.relative_url_root
end
url_base + avatar.url
end end
end end
...@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base ...@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project' belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true validates :fork_network, :project, presence: true
after_destroy :cleanup_fork_network
private
def cleanup_fork_network
# Explicitly using `#count` makes sure we have the correct number if the
# relation was loaded in the fork_network.
fork_network.destroy if fork_network.fork_network_members.count == 0
end
end end
...@@ -37,7 +37,7 @@ class Group < Namespace ...@@ -37,7 +37,7 @@ class Group < Namespace
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy` # We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`. # here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, dependent: :delete_all, foreign_key: 'entity_id' # rubocop:disable Cop/ActiveRecordDependent has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
......
...@@ -891,7 +891,19 @@ class MergeRequest < ActiveRecord::Base ...@@ -891,7 +891,19 @@ class MergeRequest < ActiveRecord::Base
# #
def all_commit_shas def all_commit_shas
if persisted? if persisted?
column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha') # MySQL doesn't support LIMIT in a subquery.
diffs_relation =
if Gitlab::Database.postgresql?
merge_request_diffs.order(id: :desc).limit(100)
else
merge_request_diffs
end
column_shas = MergeRequestDiffCommit
.where(merge_request_diff: diffs_relation)
.limit(10_000)
.pluck('sha')
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas) serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
(column_shas + serialised_shas).uniq (column_shas + serialised_shas).uniq
......
...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base ...@@ -635,6 +635,8 @@ class Project < ActiveRecord::Base
else else
super super
end end
rescue
super
end end
def valid_import_url? def valid_import_url?
......
...@@ -85,8 +85,8 @@ class ProjectWiki ...@@ -85,8 +85,8 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an # Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages. # empty Array if this Wiki has no pages.
def pages def pages(limit: nil)
wiki.pages.map { |page| WikiPage.new(self, page, true) } wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end end
# Finds a page within the repository based on a tile # Finds a page within the repository based on a tile
......
...@@ -1124,6 +1124,10 @@ class Repository ...@@ -1124,6 +1124,10 @@ class Repository
blob_data_at(sha, path) blob_data_at(sha, path)
end end
def fetch_ref(source_repository, source_ref:, target_ref:)
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
private private
# TODO Generice finder, later split this on finders by Ref or Oid # TODO Generice finder, later split this on finders by Ref or Oid
......
...@@ -127,19 +127,24 @@ class WikiPage ...@@ -127,19 +127,24 @@ class WikiPage
@version ||= @page.version @version ||= @page.version
end end
# Returns an array of Gitlab Commit instances. def versions(options = {})
def versions
return [] unless persisted? return [] unless persisted?
wiki.wiki.page_versions(@page.path) wiki.wiki.page_versions(@page.path, options)
end end
def commit def count_versions
versions.first return [] unless persisted?
wiki.wiki.count_page_versions(@page.path)
end
def last_version
@last_version ||= versions(limit: 1).first
end end
def last_commit_sha def last_commit_sha
commit&.sha last_version&.sha
end end
# Returns the Date that this latest version was # Returns the Date that this latest version was
...@@ -151,7 +156,7 @@ class WikiPage ...@@ -151,7 +156,7 @@ class WikiPage
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# is an old version of the page. # is an old version of the page.
def historical? def historical?
@page.historical? && versions.first.sha != version.sha @page.historical? && last_version.sha != version.sha
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
......
...@@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity ...@@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity
expose :state expose :state
expose :deleted_at expose :deleted_at
expose :branch_name
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
include TimeTrackableEntity include TimeTrackableEntity
prepend ::EE::MergeRequestEntity
expose :state expose :state
expose :deleted_at expose :deleted_at
...@@ -189,29 +190,6 @@ class MergeRequestEntity < IssuableEntity ...@@ -189,29 +190,6 @@ class MergeRequestEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request) commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end end
# EE-specific
expose :codeclimate, if: -> (mr, _) { mr.has_codeclimate_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :head_blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: 'codeclimate.json')
end
expose :base_blob_path, if: -> (mr, _) { mr.base_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.base_pipeline_sha)
end
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request
......
module Geo
class FilesExpireService
include ::Gitlab::Geo::LogHelpers
BATCH_SIZE = 500
attr_reader :project, :old_full_path
def initialize(project, old_full_path)
@project = project
@old_full_path = old_full_path
end
# Expire already replicated uploads
#
# This is a fallback solution to support projects that haven't rolled out to hashed-storage yet.
#
# Note: Unless we add some locking mechanism, this will be best effort only
# as if there are files that are being replicated during this execution, they will not
# be expired.
#
# The long-term solution is to use hashed storage.
def execute
return unless Gitlab::Geo.secondary?
uploads = finder.find_project_uploads(project)
log_info("Expiring replicated attachments after project rename", count: uploads.count)
schedule_file_removal(uploads)
mark_for_resync!
end
# Project's base directory for attachments storage
#
# @return base directory where all uploads for the project are stored
def base_dir
@base_dir ||= File.join(CarrierWave.root, FileUploader.base_dir, old_full_path)
end
private
def schedule_file_removal(uploads)
paths_to_remove = uploads.find_each(batch_size: BATCH_SIZE).reduce([]) do |to_remove, upload|
file_path = File.join(base_dir, upload.path)
if File.exist?(file_path)
to_remove << [file_path]
log_info("Scheduled to remove file", file_path: file_path)
end
to_remove
end
Sidekiq::Client.push_bulk('class' => Geo::FileRemovalWorker, 'args' => paths_to_remove)
end
def mark_for_resync!
finder.find_file_registries_uploads(project).delete_all
end
def finder
@finder ||= ::Geo::ExpireUploadsFinder.new
end
# This is called by LogHelpers to build json log with context info
#
# @see ::Gitlab::Geo::LogHelpers
def base_log_data(message)
{
class: self.class.name,
project_id: project.id,
project_path: project.full_path,
project_old_path: old_full_path,
message: message
}
end
end
end
...@@ -24,6 +24,7 @@ module Geo ...@@ -24,6 +24,7 @@ module Geo
def fetch_geo_node_metrics(node) def fetch_geo_node_metrics(node)
return unless node.enabled? return unless node.enabled?
return unless Gitlab::Geo.primary? || Gitlab::Metrics.prometheus_metrics_enabled?
status = node_status(node) status = node_status(node)
...@@ -33,7 +34,7 @@ module Geo ...@@ -33,7 +34,7 @@ module Geo
end end
update_db_metrics(node, status) if Gitlab::Geo.primary? update_db_metrics(node, status) if Gitlab::Geo.primary?
update_prometheus_metrics(node, status) update_prometheus_metrics(node, status) if Gitlab::Metrics.prometheus_metrics_enabled?
end end
def update_db_metrics(node, status) def update_db_metrics(node, status)
......
...@@ -14,15 +14,29 @@ module Geo ...@@ -14,15 +14,29 @@ module Geo
end end
def execute def execute
project.ensure_storage_path_exists unless move_repositories!
move_project_repository && move_wiki_repository return false
rescue end
log_error('Repository cannot be renamed')
false unless project.hashed_storage?(:attachments)
Geo::FilesExpireService.new(project, old_disk_path).execute
end
true
end end
private private
def move_repositories!
begin
project.ensure_storage_path_exists
move_project_repository && move_wiki_repository
rescue => ex
log_error('Repository cannot be renamed', error: ex)
false
end
end
def move_project_repository def move_project_repository
gitlab_shell.mv_repository(project.repository_storage_path, old_disk_path, new_disk_path) gitlab_shell.mv_repository(project.repository_storage_path, old_disk_path, new_disk_path)
end end
......
...@@ -835,5 +835,56 @@ ...@@ -835,5 +835,56 @@
The amount of seconds after which a request to get a secondary node The amount of seconds after which a request to get a secondary node
status will time out. status will time out.
%fieldset
%legend User and IP Rate Limits
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_unauthenticated_enabled do
= f.check_box :throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_api_enabled do
= f.check_box :throttle_authenticated_api_enabled
Enable authenticated API request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_web_enabled do
= f.check_box :throttle_authenticated_web_enabled
Enable authenticated web request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| = form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(application) = form_errors(application)
.form-group .form-group
......
- auth_app_owner = @pre_auth.client.application.owner
%main{ :role => "main" } %main{ :role => "main" }
.modal-no-backdrop .modal-no-backdrop.modal-doorkeepr-auth
.modal-content .modal-content
.modal-header .modal-header
%h3.page-title %h3.page-title
...@@ -16,14 +18,21 @@ ...@@ -16,14 +18,21 @@
%strong= @pre_auth.client.name %strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution. will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p %p
You are about to authorize An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer' = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
to use your account. is requesting access to your GitLab account. This application was created by
- if @pre_auth.scopes = succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
Please note that this application is not provided by GitLab and you should verify its authenticity before
allowing access.
- if @pre_auth.scopes
%p
This application will be able to: This application will be able to:
%ul %ul
- @pre_auth.scopes.each do |scope| - @pre_auth.scopes.each do |scope|
%li= t scope, scope: [:doorkeeper, :scopes] %li
%strong= t scope, scope: [:doorkeeper, :scopes]
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right .form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
......
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index']
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show') - issues_sub_menu_items.push('boards#index', 'boards#show')
...@@ -45,20 +43,8 @@ ...@@ -45,20 +43,8 @@
%span %span
Contribution Analytics Contribution Analytics
-# TODO: Add the flag check to only show epics if available
= nav_link(path: epics_items) do = render "layouts/nav/ee/epic_link", group: @group
= link_to group_epics_path(@group) do
.nav-icon-container
= sprite_icon('epic')
%span.nav-item-name
Epics
%span.badge.count= number_with_delimiter(epics.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: epics_items, html_options: { class: "fly-out-top-item" } ) do
= link_to group_epics_path(@group) do
%strong.fly-out-top-item-name
#{ _('Epics') }
%span.badge.count.epic_counter.fly-out-badge= number_with_delimiter(epics.count)
= nav_link(path: issues_sub_menu_items) do = nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do = link_to issues_group_path(@group) do
......
= webpack_bundle_tag 'add_gitlab_slack_application'
%script#js-add-gitlab-slack-application-entry-data{ type: "application/json" }
= add_to_slack_data(@projects)
#js-add-gitlab-slack-application-entry-point
%a{ href: "https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_redirect_uri(@project)}" } %a#slack-button{ href: add_to_slack_link(project, slack_app_id) }
%img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" } %img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" }
...@@ -27,4 +27,4 @@ ...@@ -27,4 +27,4 @@
= link_to 'Remove', project_settings_slack_path(project), method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' } = link_to 'Remove', project_settings_slack_path(project), method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' }
- else - else
%p To set up this service press "Add to Slack" %p To set up this service press "Add to Slack"
= render "projects/services/#{@service.to_param}/slack_button" = render "projects/services/#{@service.to_param}/slack_button", project: @project
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page) = link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format}) %small (#{wiki_page.format})
.pull-right .pull-right
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
%th= _("Last updated") %th= _("Last updated")
%th= _("Format") %th= _("Format")
%tbody %tbody
- @page.versions.each_with_index do |version, index| - @page_versions.each_with_index do |version, index|
- commit = version - commit = version
%tr %tr
%td %td
...@@ -37,5 +37,6 @@ ...@@ -37,5 +37,6 @@
%td %td
%strong %strong
= version.format = version.format
= paginate @page_versions, theme: 'gitlab'
= render 'sidebar' = render 'sidebar'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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