diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-03-21 14:22:56 +0100 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-03-21 14:22:56 +0100 |
commit | c5912ecd73560b730eda625c77d900ca23ab16d5 (patch) | |
tree | 8f7288b6209fb7e542e5d3bf867138ea6bde7faf /app | |
parent | 53d332d3c73f8a883fa54d8eaaf91f92da73c33f (diff) | |
parent | 1e5888d115df1973cd5af0aa95013dbbf29ddefd (diff) | |
download | gitlab-ce-c5912ecd73560b730eda625c77d900ca23ab16d5.tar.gz |
Merge branch 'master' into feature/multi-level-container-registry-images
* master: (1327 commits)
Merge branch 'render-json-leak' into 'security'
Merge branch 'ssrf' into 'security'
Merge branch 'ssrf' into 'security'
Merge branch 'fix-links-target-blank' into 'security'
Merge branch '28058-hide-emails-in-atom-feeds' into 'security'
Fix karma test
Reset filters after click
Handle Route#name being nil after an update
Only add frontend code coverage instrumentation when generating coverage report
fix recompile assets step in 9.0 upgrade guide to use yarn
Undo explicit conversion to Integer
Make level_value accept string integers
Make feature spec more robust
Removed d3.js from the main application.js bundle
Extend compound status for manual actions specs
Update css to be nice and tidy.
Fix pipeline status for transition between stages
add an index to the ghost column
Return 404 in project issues API endpoint when project cannot be found
Improve rename projects migration
...
Conflicts:
doc/ci/docker/using_docker_build.md
spec/lib/gitlab/import_export/all_models.yml
Diffstat (limited to 'app')
2771 files changed, 25386 insertions, 20505 deletions
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png Binary files differindex 6f1a34a5591..5dcd9c09b70 100644 --- a/app/assets/images/emoji.png +++ b/app/assets/images/emoji.png diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png Binary files differnew file mode 100644 index 00000000000..6903ff0304a --- /dev/null +++ b/app/assets/images/emoji/100.png diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png Binary files differnew file mode 100644 index 00000000000..248dc7e55b6 --- /dev/null +++ b/app/assets/images/emoji/1234.png diff --git a/app/assets/images/emoji/1F627.png b/app/assets/images/emoji/1F627.png Binary files differnew file mode 100644 index 00000000000..f99026a3bc7 --- /dev/null +++ b/app/assets/images/emoji/1F627.png diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png Binary files differnew file mode 100644 index 00000000000..38ca662eded --- /dev/null +++ b/app/assets/images/emoji/8ball.png diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png Binary files differnew file mode 100644 index 00000000000..8603ff05a17 --- /dev/null +++ b/app/assets/images/emoji/a.png diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png Binary files differnew file mode 100644 index 00000000000..d9f2d17dea0 --- /dev/null +++ b/app/assets/images/emoji/ab.png diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png Binary files differnew file mode 100644 index 00000000000..7688de692a9 --- /dev/null +++ b/app/assets/images/emoji/abc.png diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png Binary files differnew file mode 100644 index 00000000000..0996a870570 --- /dev/null +++ b/app/assets/images/emoji/abcd.png diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png Binary files differnew file mode 100644 index 00000000000..8afd7ce99cf --- /dev/null +++ b/app/assets/images/emoji/accept.png diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png Binary files differnew file mode 100644 index 00000000000..3eb4b61bf1d --- /dev/null +++ b/app/assets/images/emoji/aerial_tramway.png diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png Binary files differnew file mode 100644 index 00000000000..268d2ac3c8e --- /dev/null +++ b/app/assets/images/emoji/airplane.png diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png Binary files differnew file mode 100644 index 00000000000..d66841962f2 --- /dev/null +++ b/app/assets/images/emoji/airplane_arriving.png diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png Binary files differnew file mode 100644 index 00000000000..a5766f9f4ae --- /dev/null +++ b/app/assets/images/emoji/airplane_departure.png diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png Binary files differnew file mode 100644 index 00000000000..b731b15e3a8 --- /dev/null +++ b/app/assets/images/emoji/airplane_small.png diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png Binary files differnew file mode 100644 index 00000000000..cdbc2fbb950 --- /dev/null +++ b/app/assets/images/emoji/alarm_clock.png diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png Binary files differnew file mode 100644 index 00000000000..307a7324249 --- /dev/null +++ b/app/assets/images/emoji/alembic.png diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png Binary files differnew file mode 100644 index 00000000000..3b90e97433b --- /dev/null +++ b/app/assets/images/emoji/alien.png diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png Binary files differnew file mode 100644 index 00000000000..6fb8076d766 --- /dev/null +++ b/app/assets/images/emoji/ambulance.png diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png Binary files differnew file mode 100644 index 00000000000..96de5056059 --- /dev/null +++ b/app/assets/images/emoji/amphora.png diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png Binary files differnew file mode 100644 index 00000000000..b036f70a00b --- /dev/null +++ b/app/assets/images/emoji/anchor.png diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png Binary files differnew file mode 100644 index 00000000000..66ea97a3b99 --- /dev/null +++ b/app/assets/images/emoji/angel.png diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png Binary files differnew file mode 100644 index 00000000000..391694dc07e --- /dev/null +++ b/app/assets/images/emoji/angel_tone1.png diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png Binary files differnew file mode 100644 index 00000000000..700cbe6ed2c --- /dev/null +++ b/app/assets/images/emoji/angel_tone2.png diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png Binary files differnew file mode 100644 index 00000000000..be597437d25 --- /dev/null +++ b/app/assets/images/emoji/angel_tone3.png diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png Binary files differnew file mode 100644 index 00000000000..b06d3c853ef --- /dev/null +++ b/app/assets/images/emoji/angel_tone4.png diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png Binary files differnew file mode 100644 index 00000000000..17bd677e334 --- /dev/null +++ b/app/assets/images/emoji/angel_tone5.png diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png Binary files differnew file mode 100644 index 00000000000..d63c2e000e4 --- /dev/null +++ b/app/assets/images/emoji/anger.png diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png Binary files differnew file mode 100644 index 00000000000..f5c97c4d297 --- /dev/null +++ b/app/assets/images/emoji/anger_right.png diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png Binary files differnew file mode 100644 index 00000000000..cfc4a6ecde5 --- /dev/null +++ b/app/assets/images/emoji/angry.png diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png Binary files differnew file mode 100644 index 00000000000..994127ed6b3 --- /dev/null +++ b/app/assets/images/emoji/ant.png diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png Binary files differnew file mode 100644 index 00000000000..da650c60f62 --- /dev/null +++ b/app/assets/images/emoji/apple.png diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png Binary files differnew file mode 100644 index 00000000000..641a4f68889 --- /dev/null +++ b/app/assets/images/emoji/aquarius.png diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png Binary files differnew file mode 100644 index 00000000000..21a189d0ede --- /dev/null +++ b/app/assets/images/emoji/aries.png diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png Binary files differnew file mode 100644 index 00000000000..ee38e3b038e --- /dev/null +++ b/app/assets/images/emoji/arrow_backward.png diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png Binary files differnew file mode 100644 index 00000000000..90193bfcb40 --- /dev/null +++ b/app/assets/images/emoji/arrow_double_down.png diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png Binary files differnew file mode 100644 index 00000000000..13543d5eef2 --- /dev/null +++ b/app/assets/images/emoji/arrow_double_up.png diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png Binary files differnew file mode 100644 index 00000000000..b8eefd0b19f --- /dev/null +++ b/app/assets/images/emoji/arrow_down.png diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png Binary files differnew file mode 100644 index 00000000000..5870b9a2241 --- /dev/null +++ b/app/assets/images/emoji/arrow_down_small.png diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png Binary files differnew file mode 100644 index 00000000000..4e2b682857c --- /dev/null +++ b/app/assets/images/emoji/arrow_forward.png diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png Binary files differnew file mode 100644 index 00000000000..2d9d24bca80 --- /dev/null +++ b/app/assets/images/emoji/arrow_heading_down.png diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png Binary files differnew file mode 100644 index 00000000000..f29bfcfc0de --- /dev/null +++ b/app/assets/images/emoji/arrow_heading_up.png diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png Binary files differnew file mode 100644 index 00000000000..8c685e0a81b --- /dev/null +++ b/app/assets/images/emoji/arrow_left.png diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png Binary files differnew file mode 100644 index 00000000000..88b37716078 --- /dev/null +++ b/app/assets/images/emoji/arrow_lower_left.png diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png Binary files differnew file mode 100644 index 00000000000..7e807da7392 --- /dev/null +++ b/app/assets/images/emoji/arrow_lower_right.png diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png Binary files differnew file mode 100644 index 00000000000..4755670b5cc --- /dev/null +++ b/app/assets/images/emoji/arrow_right.png diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png Binary files differnew file mode 100644 index 00000000000..e7258ad3268 --- /dev/null +++ b/app/assets/images/emoji/arrow_right_hook.png diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png Binary files differnew file mode 100644 index 00000000000..af8218a87f7 --- /dev/null +++ b/app/assets/images/emoji/arrow_up.png diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png Binary files differnew file mode 100644 index 00000000000..dfa32b97186 --- /dev/null +++ b/app/assets/images/emoji/arrow_up_down.png diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png Binary files differnew file mode 100644 index 00000000000..20a13dcd5cd --- /dev/null +++ b/app/assets/images/emoji/arrow_up_small.png diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png Binary files differnew file mode 100644 index 00000000000..f38718fbe34 --- /dev/null +++ b/app/assets/images/emoji/arrow_upper_left.png diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png Binary files differnew file mode 100644 index 00000000000..c43e12d0f64 --- /dev/null +++ b/app/assets/images/emoji/arrow_upper_right.png diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png Binary files differnew file mode 100644 index 00000000000..26e49c38388 --- /dev/null +++ b/app/assets/images/emoji/arrows_clockwise.png diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png Binary files differnew file mode 100644 index 00000000000..8d06d8e0912 --- /dev/null +++ b/app/assets/images/emoji/arrows_counterclockwise.png diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png Binary files differnew file mode 100644 index 00000000000..bd6afe9ff06 --- /dev/null +++ b/app/assets/images/emoji/art.png diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png Binary files differnew file mode 100644 index 00000000000..c8217317132 --- /dev/null +++ b/app/assets/images/emoji/articulated_lorry.png diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png Binary files differnew file mode 100644 index 00000000000..2f8e5113803 --- /dev/null +++ b/app/assets/images/emoji/asterisk.png diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png Binary files differnew file mode 100644 index 00000000000..bd0ac55ec8e --- /dev/null +++ b/app/assets/images/emoji/astonished.png diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png Binary files differnew file mode 100644 index 00000000000..423fa07dd5d --- /dev/null +++ b/app/assets/images/emoji/athletic_shoe.png diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png Binary files differnew file mode 100644 index 00000000000..4d935307b94 --- /dev/null +++ b/app/assets/images/emoji/atm.png diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png Binary files differnew file mode 100644 index 00000000000..5f4567aa093 --- /dev/null +++ b/app/assets/images/emoji/atom.png diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png Binary files differnew file mode 100644 index 00000000000..06f0d124aed --- /dev/null +++ b/app/assets/images/emoji/avocado.png diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png Binary files differnew file mode 100644 index 00000000000..25875bc6a14 --- /dev/null +++ b/app/assets/images/emoji/b.png diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png Binary files differnew file mode 100644 index 00000000000..a4af92c63c7 --- /dev/null +++ b/app/assets/images/emoji/baby.png diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png Binary files differnew file mode 100644 index 00000000000..2bd10524180 --- /dev/null +++ b/app/assets/images/emoji/baby_bottle.png diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png Binary files differnew file mode 100644 index 00000000000..dccd96576ea --- /dev/null +++ b/app/assets/images/emoji/baby_chick.png diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png Binary files differnew file mode 100644 index 00000000000..64a10b71710 --- /dev/null +++ b/app/assets/images/emoji/baby_symbol.png diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png Binary files differnew file mode 100644 index 00000000000..d20911d40db --- /dev/null +++ b/app/assets/images/emoji/baby_tone1.png diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png Binary files differnew file mode 100644 index 00000000000..b0a9b30ed17 --- /dev/null +++ b/app/assets/images/emoji/baby_tone2.png diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png Binary files differnew file mode 100644 index 00000000000..7de5286fac1 --- /dev/null +++ b/app/assets/images/emoji/baby_tone3.png diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png Binary files differnew file mode 100644 index 00000000000..9b7a86ac615 --- /dev/null +++ b/app/assets/images/emoji/baby_tone4.png diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png Binary files differnew file mode 100644 index 00000000000..fe1be34cb88 --- /dev/null +++ b/app/assets/images/emoji/baby_tone5.png diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png Binary files differnew file mode 100644 index 00000000000..d32c5d4f17f --- /dev/null +++ b/app/assets/images/emoji/back.png diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png Binary files differnew file mode 100644 index 00000000000..f38a485fbe4 --- /dev/null +++ b/app/assets/images/emoji/bacon.png diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png Binary files differnew file mode 100644 index 00000000000..7ba15708990 --- /dev/null +++ b/app/assets/images/emoji/badminton.png diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png Binary files differnew file mode 100644 index 00000000000..409b593e78a --- /dev/null +++ b/app/assets/images/emoji/baggage_claim.png diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png Binary files differnew file mode 100644 index 00000000000..07916fe6df1 --- /dev/null +++ b/app/assets/images/emoji/balloon.png diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png Binary files differnew file mode 100644 index 00000000000..9b6767aea9e --- /dev/null +++ b/app/assets/images/emoji/ballot_box.png diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png Binary files differnew file mode 100644 index 00000000000..284d9573847 --- /dev/null +++ b/app/assets/images/emoji/ballot_box_with_check.png diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png Binary files differnew file mode 100644 index 00000000000..5d5e0e728a0 --- /dev/null +++ b/app/assets/images/emoji/bamboo.png diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png Binary files differnew file mode 100644 index 00000000000..f4987279580 --- /dev/null +++ b/app/assets/images/emoji/banana.png diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png Binary files differnew file mode 100644 index 00000000000..58a9c528fca --- /dev/null +++ b/app/assets/images/emoji/bangbang.png diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png Binary files differnew file mode 100644 index 00000000000..dffdcef36a1 --- /dev/null +++ b/app/assets/images/emoji/bank.png diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png Binary files differnew file mode 100644 index 00000000000..53c89455008 --- /dev/null +++ b/app/assets/images/emoji/bar_chart.png diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png Binary files differnew file mode 100644 index 00000000000..896f4d716cf --- /dev/null +++ b/app/assets/images/emoji/barber.png diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png Binary files differnew file mode 100644 index 00000000000..f8463f1538b --- /dev/null +++ b/app/assets/images/emoji/baseball.png diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png Binary files differnew file mode 100644 index 00000000000..64c76b79c6d --- /dev/null +++ b/app/assets/images/emoji/basketball.png diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png Binary files differnew file mode 100644 index 00000000000..8ce90c5cad6 --- /dev/null +++ b/app/assets/images/emoji/basketball_player.png diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png Binary files differnew file mode 100644 index 00000000000..cd12c7ab9bf --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone1.png diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png Binary files differnew file mode 100644 index 00000000000..f892fd596da --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone2.png diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png Binary files differnew file mode 100644 index 00000000000..e109997a91a --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone3.png diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png Binary files differnew file mode 100644 index 00000000000..3b90b946af4 --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone4.png diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png Binary files differnew file mode 100644 index 00000000000..bafed7828a7 --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone5.png diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png Binary files differnew file mode 100644 index 00000000000..3152c047e00 --- /dev/null +++ b/app/assets/images/emoji/bat.png diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png Binary files differnew file mode 100644 index 00000000000..43fba5c8a28 --- /dev/null +++ b/app/assets/images/emoji/bath.png diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png Binary files differnew file mode 100644 index 00000000000..2152eabf2f5 --- /dev/null +++ b/app/assets/images/emoji/bath_tone1.png diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png Binary files differnew file mode 100644 index 00000000000..2102e6133e3 --- /dev/null +++ b/app/assets/images/emoji/bath_tone2.png diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png Binary files differnew file mode 100644 index 00000000000..fae66181e9f --- /dev/null +++ b/app/assets/images/emoji/bath_tone3.png diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png Binary files differnew file mode 100644 index 00000000000..1f8959d0d99 --- /dev/null +++ b/app/assets/images/emoji/bath_tone4.png diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png Binary files differnew file mode 100644 index 00000000000..c8a08e84f25 --- /dev/null +++ b/app/assets/images/emoji/bath_tone5.png diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png Binary files differnew file mode 100644 index 00000000000..9a5f09361eb --- /dev/null +++ b/app/assets/images/emoji/bathtub.png diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png Binary files differnew file mode 100644 index 00000000000..f593e2bdb65 --- /dev/null +++ b/app/assets/images/emoji/battery.png diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png Binary files differnew file mode 100644 index 00000000000..69108c8ea10 --- /dev/null +++ b/app/assets/images/emoji/beach.png diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png Binary files differnew file mode 100644 index 00000000000..220a74f8132 --- /dev/null +++ b/app/assets/images/emoji/beach_umbrella.png diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png Binary files differnew file mode 100644 index 00000000000..272d56bbbcc --- /dev/null +++ b/app/assets/images/emoji/bear.png diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png Binary files differnew file mode 100644 index 00000000000..86f964e245d --- /dev/null +++ b/app/assets/images/emoji/bed.png diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png Binary files differnew file mode 100644 index 00000000000..46156060096 --- /dev/null +++ b/app/assets/images/emoji/bee.png diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png Binary files differnew file mode 100644 index 00000000000..b6d73dc0b7a --- /dev/null +++ b/app/assets/images/emoji/beer.png diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png Binary files differnew file mode 100644 index 00000000000..b55deb66b41 --- /dev/null +++ b/app/assets/images/emoji/beers.png diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png Binary files differnew file mode 100644 index 00000000000..3d93174d7fc --- /dev/null +++ b/app/assets/images/emoji/beetle.png diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png Binary files differnew file mode 100644 index 00000000000..bc434fb7cb5 --- /dev/null +++ b/app/assets/images/emoji/beginner.png diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png Binary files differnew file mode 100644 index 00000000000..5b3b0461999 --- /dev/null +++ b/app/assets/images/emoji/bell.png diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png Binary files differnew file mode 100644 index 00000000000..6b3297ceaf7 --- /dev/null +++ b/app/assets/images/emoji/bellhop.png diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png Binary files differnew file mode 100644 index 00000000000..83d41ca7eb9 --- /dev/null +++ b/app/assets/images/emoji/bento.png diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png Binary files differnew file mode 100644 index 00000000000..9274da11048 --- /dev/null +++ b/app/assets/images/emoji/bicyclist.png diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png Binary files differnew file mode 100644 index 00000000000..decc2f728fe --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone1.png diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png Binary files differnew file mode 100644 index 00000000000..0067717b80a --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone2.png diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png Binary files differnew file mode 100644 index 00000000000..a4f7b5e2776 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone3.png diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png Binary files differnew file mode 100644 index 00000000000..a3c8a797db4 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone4.png diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png Binary files differnew file mode 100644 index 00000000000..1606a874051 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone5.png diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png Binary files differnew file mode 100644 index 00000000000..556ed70f1a7 --- /dev/null +++ b/app/assets/images/emoji/bike.png diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png Binary files differnew file mode 100644 index 00000000000..77a8a0aae5b --- /dev/null +++ b/app/assets/images/emoji/bikini.png diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png Binary files differnew file mode 100644 index 00000000000..007b4fc2d85 --- /dev/null +++ b/app/assets/images/emoji/biohazard.png diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png Binary files differnew file mode 100644 index 00000000000..e201c22be33 --- /dev/null +++ b/app/assets/images/emoji/bird.png diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png Binary files differnew file mode 100644 index 00000000000..317e9a41949 --- /dev/null +++ b/app/assets/images/emoji/birthday.png diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png Binary files differnew file mode 100644 index 00000000000..b62b87170e8 --- /dev/null +++ b/app/assets/images/emoji/black_circle.png diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png Binary files differnew file mode 100644 index 00000000000..b4068c3e6e8 --- /dev/null +++ b/app/assets/images/emoji/black_heart.png diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png Binary files differnew file mode 100644 index 00000000000..3d0924b68aa --- /dev/null +++ b/app/assets/images/emoji/black_joker.png diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png Binary files differnew file mode 100644 index 00000000000..162f2bb4290 --- /dev/null +++ b/app/assets/images/emoji/black_large_square.png diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png Binary files differnew file mode 100644 index 00000000000..39765bba610 --- /dev/null +++ b/app/assets/images/emoji/black_medium_small_square.png diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png Binary files differnew file mode 100644 index 00000000000..05a30a6aa2d --- /dev/null +++ b/app/assets/images/emoji/black_medium_square.png diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png Binary files differnew file mode 100644 index 00000000000..872d0ae1598 --- /dev/null +++ b/app/assets/images/emoji/black_nib.png diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png Binary files differnew file mode 100644 index 00000000000..48595d3e1a9 --- /dev/null +++ b/app/assets/images/emoji/black_small_square.png diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png Binary files differnew file mode 100644 index 00000000000..a78fc2f6b63 --- /dev/null +++ b/app/assets/images/emoji/black_square_button.png diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png Binary files differnew file mode 100644 index 00000000000..4083026c157 --- /dev/null +++ b/app/assets/images/emoji/blossom.png diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png Binary files differnew file mode 100644 index 00000000000..a10f4f84e35 --- /dev/null +++ b/app/assets/images/emoji/blowfish.png diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png Binary files differnew file mode 100644 index 00000000000..e1e455401cc --- /dev/null +++ b/app/assets/images/emoji/blue_book.png diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png Binary files differnew file mode 100644 index 00000000000..e8ba817d393 --- /dev/null +++ b/app/assets/images/emoji/blue_car.png diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png Binary files differnew file mode 100644 index 00000000000..bdf1287e55e --- /dev/null +++ b/app/assets/images/emoji/blue_heart.png diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png Binary files differnew file mode 100644 index 00000000000..aac1a424ad4 --- /dev/null +++ b/app/assets/images/emoji/blush.png diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png Binary files differnew file mode 100644 index 00000000000..fead972633c --- /dev/null +++ b/app/assets/images/emoji/boar.png diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png Binary files differnew file mode 100644 index 00000000000..c7f8f81c939 --- /dev/null +++ b/app/assets/images/emoji/bomb.png diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png Binary files differnew file mode 100644 index 00000000000..0f4447ed396 --- /dev/null +++ b/app/assets/images/emoji/book.png diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png Binary files differnew file mode 100644 index 00000000000..bbb444611f0 --- /dev/null +++ b/app/assets/images/emoji/bookmark.png diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png Binary files differnew file mode 100644 index 00000000000..f8d9e01b428 --- /dev/null +++ b/app/assets/images/emoji/bookmark_tabs.png diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png Binary files differnew file mode 100644 index 00000000000..59a8bafeb0d --- /dev/null +++ b/app/assets/images/emoji/books.png diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png Binary files differnew file mode 100644 index 00000000000..9b0f027b1a8 --- /dev/null +++ b/app/assets/images/emoji/boom.png diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png Binary files differnew file mode 100644 index 00000000000..11f1065ed07 --- /dev/null +++ b/app/assets/images/emoji/boot.png diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png Binary files differnew file mode 100644 index 00000000000..11455af6df4 --- /dev/null +++ b/app/assets/images/emoji/bouquet.png diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png Binary files differnew file mode 100644 index 00000000000..d8f793088dc --- /dev/null +++ b/app/assets/images/emoji/bow.png diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png Binary files differnew file mode 100644 index 00000000000..6a538bf475f --- /dev/null +++ b/app/assets/images/emoji/bow_and_arrow.png diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png Binary files differnew file mode 100644 index 00000000000..87afb7b54cf --- /dev/null +++ b/app/assets/images/emoji/bow_tone1.png diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png Binary files differnew file mode 100644 index 00000000000..3ccf7dc0850 --- /dev/null +++ b/app/assets/images/emoji/bow_tone2.png diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png Binary files differnew file mode 100644 index 00000000000..8b9eb64f926 --- /dev/null +++ b/app/assets/images/emoji/bow_tone3.png diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png Binary files differnew file mode 100644 index 00000000000..683795ff40d --- /dev/null +++ b/app/assets/images/emoji/bow_tone4.png diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png Binary files differnew file mode 100644 index 00000000000..7969d971752 --- /dev/null +++ b/app/assets/images/emoji/bow_tone5.png diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png Binary files differnew file mode 100644 index 00000000000..63add89e53b --- /dev/null +++ b/app/assets/images/emoji/bowling.png diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png Binary files differnew file mode 100644 index 00000000000..9838f24e51a --- /dev/null +++ b/app/assets/images/emoji/boxing_glove.png diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png Binary files differnew file mode 100644 index 00000000000..8ecfb0a4e92 --- /dev/null +++ b/app/assets/images/emoji/boy.png diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png Binary files differnew file mode 100644 index 00000000000..2fc436ea512 --- /dev/null +++ b/app/assets/images/emoji/boy_tone1.png diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png Binary files differnew file mode 100644 index 00000000000..09a5f18d360 --- /dev/null +++ b/app/assets/images/emoji/boy_tone2.png diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png Binary files differnew file mode 100644 index 00000000000..3cfe675dd3a --- /dev/null +++ b/app/assets/images/emoji/boy_tone3.png diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png Binary files differnew file mode 100644 index 00000000000..780be0ace36 --- /dev/null +++ b/app/assets/images/emoji/boy_tone4.png diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png Binary files differnew file mode 100644 index 00000000000..f32fe22e35c --- /dev/null +++ b/app/assets/images/emoji/boy_tone5.png diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png Binary files differnew file mode 100644 index 00000000000..6676510aaa5 --- /dev/null +++ b/app/assets/images/emoji/bread.png diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png Binary files differnew file mode 100644 index 00000000000..eaf4bd97890 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil.png diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png Binary files differnew file mode 100644 index 00000000000..c4fb141ae8f --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone1.png diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png Binary files differnew file mode 100644 index 00000000000..c248769fc06 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone2.png diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png Binary files differnew file mode 100644 index 00000000000..962c0a6eedb --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone3.png diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png Binary files differnew file mode 100644 index 00000000000..740ca208cd4 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone4.png diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png Binary files differnew file mode 100644 index 00000000000..5cc5598587d --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone5.png diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png Binary files differnew file mode 100644 index 00000000000..1d444e0be65 --- /dev/null +++ b/app/assets/images/emoji/bridge_at_night.png diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png Binary files differnew file mode 100644 index 00000000000..b9912ba2148 --- /dev/null +++ b/app/assets/images/emoji/briefcase.png diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png Binary files differnew file mode 100644 index 00000000000..718e26ee122 --- /dev/null +++ b/app/assets/images/emoji/broken_heart.png diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png Binary files differnew file mode 100644 index 00000000000..e64e72f259a --- /dev/null +++ b/app/assets/images/emoji/bug.png diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png Binary files differnew file mode 100644 index 00000000000..38e32e02d9f --- /dev/null +++ b/app/assets/images/emoji/bulb.png diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png Binary files differnew file mode 100644 index 00000000000..4f698e056fa --- /dev/null +++ b/app/assets/images/emoji/bullettrain_front.png diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png Binary files differnew file mode 100644 index 00000000000..ed61c67bf07 --- /dev/null +++ b/app/assets/images/emoji/bullettrain_side.png diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png Binary files differnew file mode 100644 index 00000000000..02bd5601df7 --- /dev/null +++ b/app/assets/images/emoji/burrito.png diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png Binary files differnew file mode 100644 index 00000000000..641ddc56ca7 --- /dev/null +++ b/app/assets/images/emoji/bus.png diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png Binary files differnew file mode 100644 index 00000000000..b2b62208bfd --- /dev/null +++ b/app/assets/images/emoji/busstop.png diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png Binary files differnew file mode 100644 index 00000000000..123b2cbe1fb --- /dev/null +++ b/app/assets/images/emoji/bust_in_silhouette.png diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png Binary files differnew file mode 100644 index 00000000000..d7656860a1c --- /dev/null +++ b/app/assets/images/emoji/busts_in_silhouette.png diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png Binary files differnew file mode 100644 index 00000000000..5631fe99226 --- /dev/null +++ b/app/assets/images/emoji/butterfly.png diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png Binary files differnew file mode 100644 index 00000000000..9b48ccf3d0c --- /dev/null +++ b/app/assets/images/emoji/cactus.png diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png Binary files differnew file mode 100644 index 00000000000..4368177be9a --- /dev/null +++ b/app/assets/images/emoji/cake.png diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png Binary files differnew file mode 100644 index 00000000000..47353b74447 --- /dev/null +++ b/app/assets/images/emoji/calendar.png diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png Binary files differnew file mode 100644 index 00000000000..dec8d49bfa8 --- /dev/null +++ b/app/assets/images/emoji/calendar_spiral.png diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png Binary files differnew file mode 100644 index 00000000000..a10c59ba711 --- /dev/null +++ b/app/assets/images/emoji/call_me.png diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png Binary files differnew file mode 100644 index 00000000000..2c93201181a --- /dev/null +++ b/app/assets/images/emoji/call_me_tone1.png diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png Binary files differnew file mode 100644 index 00000000000..c39f45a41ed --- /dev/null +++ b/app/assets/images/emoji/call_me_tone2.png diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png Binary files differnew file mode 100644 index 00000000000..83a57f63c29 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone3.png diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png Binary files differnew file mode 100644 index 00000000000..65b3468fe44 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone4.png diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png Binary files differnew file mode 100644 index 00000000000..94ef68ff3b3 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone5.png diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png Binary files differnew file mode 100644 index 00000000000..e2f308f8e46 --- /dev/null +++ b/app/assets/images/emoji/calling.png diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png Binary files differnew file mode 100644 index 00000000000..b421d07a805 --- /dev/null +++ b/app/assets/images/emoji/camel.png diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png Binary files differnew file mode 100644 index 00000000000..0a3429f72ef --- /dev/null +++ b/app/assets/images/emoji/camera.png diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png Binary files differnew file mode 100644 index 00000000000..27471da2029 --- /dev/null +++ b/app/assets/images/emoji/camera_with_flash.png diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png Binary files differnew file mode 100644 index 00000000000..d589cc1f44b --- /dev/null +++ b/app/assets/images/emoji/camping.png diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png Binary files differnew file mode 100644 index 00000000000..a64af07cb5f --- /dev/null +++ b/app/assets/images/emoji/cancer.png diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png Binary files differnew file mode 100644 index 00000000000..0b56444e355 --- /dev/null +++ b/app/assets/images/emoji/candle.png diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png Binary files differnew file mode 100644 index 00000000000..8c67ace3a35 --- /dev/null +++ b/app/assets/images/emoji/candy.png diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png Binary files differnew file mode 100644 index 00000000000..e26cdb9da69 --- /dev/null +++ b/app/assets/images/emoji/canoe.png diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png Binary files differnew file mode 100644 index 00000000000..fe9482d2d8a --- /dev/null +++ b/app/assets/images/emoji/capital_abcd.png diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png Binary files differnew file mode 100644 index 00000000000..6293d31d4b1 --- /dev/null +++ b/app/assets/images/emoji/capricorn.png diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png Binary files differnew file mode 100644 index 00000000000..f2e764ce59d --- /dev/null +++ b/app/assets/images/emoji/card_box.png diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png Binary files differnew file mode 100644 index 00000000000..151e11cb3b4 --- /dev/null +++ b/app/assets/images/emoji/card_index.png diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png Binary files differnew file mode 100644 index 00000000000..a17074edf05 --- /dev/null +++ b/app/assets/images/emoji/carousel_horse.png diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png Binary files differnew file mode 100644 index 00000000000..c68829b58e7 --- /dev/null +++ b/app/assets/images/emoji/carrot.png diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png Binary files differnew file mode 100644 index 00000000000..cbcaa578253 --- /dev/null +++ b/app/assets/images/emoji/cartwheel.png diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png Binary files differnew file mode 100644 index 00000000000..db6d65895fb --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone1.png diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png Binary files differnew file mode 100644 index 00000000000..e00ffbc27a8 --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone2.png diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png Binary files differnew file mode 100644 index 00000000000..49321be391f --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone3.png diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png Binary files differnew file mode 100644 index 00000000000..d4562b5e3dd --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone4.png diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png Binary files differnew file mode 100644 index 00000000000..6e09a870767 --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone5.png diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png Binary files differnew file mode 100644 index 00000000000..efd82c2abf3 --- /dev/null +++ b/app/assets/images/emoji/cat.png diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png Binary files differnew file mode 100644 index 00000000000..46abe8cbc14 --- /dev/null +++ b/app/assets/images/emoji/cat2.png diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png Binary files differnew file mode 100644 index 00000000000..e6b01449cd9 --- /dev/null +++ b/app/assets/images/emoji/cd.png diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png Binary files differnew file mode 100644 index 00000000000..57f46139a06 --- /dev/null +++ b/app/assets/images/emoji/chains.png diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png Binary files differnew file mode 100644 index 00000000000..285a79a93d0 --- /dev/null +++ b/app/assets/images/emoji/champagne.png diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png Binary files differnew file mode 100644 index 00000000000..31937ae9392 --- /dev/null +++ b/app/assets/images/emoji/champagne_glass.png diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png Binary files differnew file mode 100644 index 00000000000..9773f03be22 --- /dev/null +++ b/app/assets/images/emoji/chart.png diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png Binary files differnew file mode 100644 index 00000000000..5222ec72d85 --- /dev/null +++ b/app/assets/images/emoji/chart_with_downwards_trend.png diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png Binary files differnew file mode 100644 index 00000000000..f13cfcf9956 --- /dev/null +++ b/app/assets/images/emoji/chart_with_upwards_trend.png diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png Binary files differnew file mode 100644 index 00000000000..5a71eecb89b --- /dev/null +++ b/app/assets/images/emoji/checkered_flag.png diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png Binary files differnew file mode 100644 index 00000000000..00e99762286 --- /dev/null +++ b/app/assets/images/emoji/cheese.png diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png Binary files differnew file mode 100644 index 00000000000..9b10cbaac5e --- /dev/null +++ b/app/assets/images/emoji/cherries.png diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png Binary files differnew file mode 100644 index 00000000000..282f3e7bc81 --- /dev/null +++ b/app/assets/images/emoji/cherry_blossom.png diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png Binary files differnew file mode 100644 index 00000000000..e9fb40468ed --- /dev/null +++ b/app/assets/images/emoji/chestnut.png diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png Binary files differnew file mode 100644 index 00000000000..9a6992e55ba --- /dev/null +++ b/app/assets/images/emoji/chicken.png diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png Binary files differnew file mode 100644 index 00000000000..fa4c091c7c3 --- /dev/null +++ b/app/assets/images/emoji/children_crossing.png diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png Binary files differnew file mode 100644 index 00000000000..2aac560cb22 --- /dev/null +++ b/app/assets/images/emoji/chipmunk.png diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png Binary files differnew file mode 100644 index 00000000000..318bbd40ef9 --- /dev/null +++ b/app/assets/images/emoji/chocolate_bar.png diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png Binary files differnew file mode 100644 index 00000000000..4197d37a52b --- /dev/null +++ b/app/assets/images/emoji/christmas_tree.png diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png Binary files differnew file mode 100644 index 00000000000..8242fd272b3 --- /dev/null +++ b/app/assets/images/emoji/church.png diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png Binary files differnew file mode 100644 index 00000000000..65f27b386f2 --- /dev/null +++ b/app/assets/images/emoji/cinema.png diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png Binary files differnew file mode 100644 index 00000000000..b0379775b12 --- /dev/null +++ b/app/assets/images/emoji/circus_tent.png diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png Binary files differnew file mode 100644 index 00000000000..80cdff7cf5d --- /dev/null +++ b/app/assets/images/emoji/city_dusk.png diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png Binary files differnew file mode 100644 index 00000000000..7cded0ba55b --- /dev/null +++ b/app/assets/images/emoji/city_sunset.png diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png Binary files differnew file mode 100644 index 00000000000..d7b9844a0b4 --- /dev/null +++ b/app/assets/images/emoji/cityscape.png diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png Binary files differnew file mode 100644 index 00000000000..8b01b4343e2 --- /dev/null +++ b/app/assets/images/emoji/cl.png diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png Binary files differnew file mode 100644 index 00000000000..b0ffe928920 --- /dev/null +++ b/app/assets/images/emoji/clap.png diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png Binary files differnew file mode 100644 index 00000000000..de4bc837b96 --- /dev/null +++ b/app/assets/images/emoji/clap_tone1.png diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png Binary files differnew file mode 100644 index 00000000000..1323de775ba --- /dev/null +++ b/app/assets/images/emoji/clap_tone2.png diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png Binary files differnew file mode 100644 index 00000000000..d448ca19dde --- /dev/null +++ b/app/assets/images/emoji/clap_tone3.png diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png Binary files differnew file mode 100644 index 00000000000..c49f44ee91d --- /dev/null +++ b/app/assets/images/emoji/clap_tone4.png diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png Binary files differnew file mode 100644 index 00000000000..29ee9bdf37c --- /dev/null +++ b/app/assets/images/emoji/clap_tone5.png diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png Binary files differnew file mode 100644 index 00000000000..81390883111 --- /dev/null +++ b/app/assets/images/emoji/clapper.png diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png Binary files differnew file mode 100644 index 00000000000..de7b559daaf --- /dev/null +++ b/app/assets/images/emoji/classical_building.png diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png Binary files differnew file mode 100644 index 00000000000..7edcfc52509 --- /dev/null +++ b/app/assets/images/emoji/clipboard.png diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png Binary files differnew file mode 100644 index 00000000000..ffdb451e3a8 --- /dev/null +++ b/app/assets/images/emoji/clock.png diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png Binary files differnew file mode 100644 index 00000000000..d6e34941f23 --- /dev/null +++ b/app/assets/images/emoji/clock1.png diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png Binary files differnew file mode 100644 index 00000000000..e62b245cdbe --- /dev/null +++ b/app/assets/images/emoji/clock10.png diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png Binary files differnew file mode 100644 index 00000000000..0802b3c65b9 --- /dev/null +++ b/app/assets/images/emoji/clock1030.png diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png Binary files differnew file mode 100644 index 00000000000..0983345273b --- /dev/null +++ b/app/assets/images/emoji/clock11.png diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png Binary files differnew file mode 100644 index 00000000000..d970d03b809 --- /dev/null +++ b/app/assets/images/emoji/clock1130.png diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png Binary files differnew file mode 100644 index 00000000000..e61caa4b3e2 --- /dev/null +++ b/app/assets/images/emoji/clock12.png diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png Binary files differnew file mode 100644 index 00000000000..f2b1d261721 --- /dev/null +++ b/app/assets/images/emoji/clock1230.png diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png Binary files differnew file mode 100644 index 00000000000..86b7689b84e --- /dev/null +++ b/app/assets/images/emoji/clock130.png diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png Binary files differnew file mode 100644 index 00000000000..a54253d7d57 --- /dev/null +++ b/app/assets/images/emoji/clock2.png diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png Binary files differnew file mode 100644 index 00000000000..7a787e018e6 --- /dev/null +++ b/app/assets/images/emoji/clock230.png diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png Binary files differnew file mode 100644 index 00000000000..27ec4b1f514 --- /dev/null +++ b/app/assets/images/emoji/clock3.png diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png Binary files differnew file mode 100644 index 00000000000..c6860395cec --- /dev/null +++ b/app/assets/images/emoji/clock330.png diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png Binary files differnew file mode 100644 index 00000000000..60a1ef4cc13 --- /dev/null +++ b/app/assets/images/emoji/clock4.png diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png Binary files differnew file mode 100644 index 00000000000..3c05b362122 --- /dev/null +++ b/app/assets/images/emoji/clock430.png diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png Binary files differnew file mode 100644 index 00000000000..c9382d1e094 --- /dev/null +++ b/app/assets/images/emoji/clock5.png diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png Binary files differnew file mode 100644 index 00000000000..c21fa926db2 --- /dev/null +++ b/app/assets/images/emoji/clock530.png diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png Binary files differnew file mode 100644 index 00000000000..8fd5d3f5bd7 --- /dev/null +++ b/app/assets/images/emoji/clock6.png diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png Binary files differnew file mode 100644 index 00000000000..2aec87fefcf --- /dev/null +++ b/app/assets/images/emoji/clock630.png diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png Binary files differnew file mode 100644 index 00000000000..8c7084036f2 --- /dev/null +++ b/app/assets/images/emoji/clock7.png diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png Binary files differnew file mode 100644 index 00000000000..f7a1135e03f --- /dev/null +++ b/app/assets/images/emoji/clock730.png diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png Binary files differnew file mode 100644 index 00000000000..fcddf722e95 --- /dev/null +++ b/app/assets/images/emoji/clock8.png diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png Binary files differnew file mode 100644 index 00000000000..799b4aebc08 --- /dev/null +++ b/app/assets/images/emoji/clock830.png diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png Binary files differnew file mode 100644 index 00000000000..dfbe0117981 --- /dev/null +++ b/app/assets/images/emoji/clock9.png diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png Binary files differnew file mode 100644 index 00000000000..4a2092ee6f0 --- /dev/null +++ b/app/assets/images/emoji/clock930.png diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png Binary files differnew file mode 100644 index 00000000000..6395cf2151e --- /dev/null +++ b/app/assets/images/emoji/closed_book.png diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png Binary files differnew file mode 100644 index 00000000000..1c1cd5d0741 --- /dev/null +++ b/app/assets/images/emoji/closed_lock_with_key.png diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png Binary files differnew file mode 100644 index 00000000000..ecefba9e446 --- /dev/null +++ b/app/assets/images/emoji/closed_umbrella.png diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png Binary files differnew file mode 100644 index 00000000000..5b4f57f77ba --- /dev/null +++ b/app/assets/images/emoji/cloud.png diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png Binary files differnew file mode 100644 index 00000000000..0831e88aa31 --- /dev/null +++ b/app/assets/images/emoji/cloud_lightning.png diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png Binary files differnew file mode 100644 index 00000000000..385685e0512 --- /dev/null +++ b/app/assets/images/emoji/cloud_rain.png diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png Binary files differnew file mode 100644 index 00000000000..9720384eb99 --- /dev/null +++ b/app/assets/images/emoji/cloud_snow.png diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png Binary files differnew file mode 100644 index 00000000000..4821c89da1e --- /dev/null +++ b/app/assets/images/emoji/cloud_tornado.png diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png Binary files differnew file mode 100644 index 00000000000..02b7ff70049 --- /dev/null +++ b/app/assets/images/emoji/clown.png diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png Binary files differnew file mode 100644 index 00000000000..4f2abf791ca --- /dev/null +++ b/app/assets/images/emoji/clubs.png diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png Binary files differnew file mode 100644 index 00000000000..2e50c57e98d --- /dev/null +++ b/app/assets/images/emoji/cocktail.png diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png Binary files differnew file mode 100644 index 00000000000..553061471b1 --- /dev/null +++ b/app/assets/images/emoji/coffee.png diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png Binary files differnew file mode 100644 index 00000000000..fb2932aa5f6 --- /dev/null +++ b/app/assets/images/emoji/coffin.png diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png Binary files differnew file mode 100644 index 00000000000..85b2231bbf6 --- /dev/null +++ b/app/assets/images/emoji/cold_sweat.png diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png Binary files differnew file mode 100644 index 00000000000..a99751f79be --- /dev/null +++ b/app/assets/images/emoji/comet.png diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png Binary files differnew file mode 100644 index 00000000000..d7eda7f362a --- /dev/null +++ b/app/assets/images/emoji/compression.png diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png Binary files differnew file mode 100644 index 00000000000..c1fee27e3a9 --- /dev/null +++ b/app/assets/images/emoji/computer.png diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png Binary files differnew file mode 100644 index 00000000000..ba4fd9b12be --- /dev/null +++ b/app/assets/images/emoji/confetti_ball.png diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png Binary files differnew file mode 100644 index 00000000000..aa4b29e9375 --- /dev/null +++ b/app/assets/images/emoji/confounded.png diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png Binary files differnew file mode 100644 index 00000000000..502b6bf0e0b --- /dev/null +++ b/app/assets/images/emoji/confused.png diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png Binary files differnew file mode 100644 index 00000000000..ba8c89d95ee --- /dev/null +++ b/app/assets/images/emoji/congratulations.png diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png Binary files differnew file mode 100644 index 00000000000..ef8db5f471c --- /dev/null +++ b/app/assets/images/emoji/construction.png diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png Binary files differnew file mode 100644 index 00000000000..8206a20f63f --- /dev/null +++ b/app/assets/images/emoji/construction_site.png diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png Binary files differnew file mode 100644 index 00000000000..a9970a89005 --- /dev/null +++ b/app/assets/images/emoji/construction_worker.png diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png Binary files differnew file mode 100644 index 00000000000..2f24a2bab24 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone1.png diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png Binary files differnew file mode 100644 index 00000000000..93c8fec5a75 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone2.png diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png Binary files differnew file mode 100644 index 00000000000..abc1f2af2e0 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone3.png diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png Binary files differnew file mode 100644 index 00000000000..eed83289aeb --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone4.png diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png Binary files differnew file mode 100644 index 00000000000..acbb220b8bb --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone5.png diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png Binary files differnew file mode 100644 index 00000000000..6635ac93b50 --- /dev/null +++ b/app/assets/images/emoji/control_knobs.png diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png Binary files differnew file mode 100644 index 00000000000..26b53b5669e --- /dev/null +++ b/app/assets/images/emoji/convenience_store.png diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png Binary files differnew file mode 100644 index 00000000000..1b6bcb1554f --- /dev/null +++ b/app/assets/images/emoji/cookie.png diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png Binary files differnew file mode 100644 index 00000000000..918c980577a --- /dev/null +++ b/app/assets/images/emoji/cooking.png diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png Binary files differnew file mode 100644 index 00000000000..74674978d00 --- /dev/null +++ b/app/assets/images/emoji/cool.png diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png Binary files differnew file mode 100644 index 00000000000..0b16d7c17b7 --- /dev/null +++ b/app/assets/images/emoji/cop.png diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png Binary files differnew file mode 100644 index 00000000000..6ccba3879dc --- /dev/null +++ b/app/assets/images/emoji/cop_tone1.png diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png Binary files differnew file mode 100644 index 00000000000..7814ea9f52d --- /dev/null +++ b/app/assets/images/emoji/cop_tone2.png diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png Binary files differnew file mode 100644 index 00000000000..d78e88ec872 --- /dev/null +++ b/app/assets/images/emoji/cop_tone3.png diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png Binary files differnew file mode 100644 index 00000000000..2e13c508315 --- /dev/null +++ b/app/assets/images/emoji/cop_tone4.png diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png Binary files differnew file mode 100644 index 00000000000..2980d61cc2e --- /dev/null +++ b/app/assets/images/emoji/cop_tone5.png diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png Binary files differnew file mode 100644 index 00000000000..6b9a6adbfd2 --- /dev/null +++ b/app/assets/images/emoji/copyright.png diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png Binary files differnew file mode 100644 index 00000000000..36e20127931 --- /dev/null +++ b/app/assets/images/emoji/corn.png diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png Binary files differnew file mode 100644 index 00000000000..27b19b13bb0 --- /dev/null +++ b/app/assets/images/emoji/couch.png diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png Binary files differnew file mode 100644 index 00000000000..960323f3c16 --- /dev/null +++ b/app/assets/images/emoji/couple.png diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png Binary files differnew file mode 100644 index 00000000000..8759fa5db87 --- /dev/null +++ b/app/assets/images/emoji/couple_mm.png diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png Binary files differnew file mode 100644 index 00000000000..62111601b36 --- /dev/null +++ b/app/assets/images/emoji/couple_with_heart.png diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png Binary files differnew file mode 100644 index 00000000000..08fdabcdc5c --- /dev/null +++ b/app/assets/images/emoji/couple_ww.png diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png Binary files differnew file mode 100644 index 00000000000..9aa519da9e8 --- /dev/null +++ b/app/assets/images/emoji/couplekiss.png diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png Binary files differnew file mode 100644 index 00000000000..718a3986d64 --- /dev/null +++ b/app/assets/images/emoji/cow.png diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png Binary files differnew file mode 100644 index 00000000000..4d0ca534ff1 --- /dev/null +++ b/app/assets/images/emoji/cow2.png diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png Binary files differnew file mode 100644 index 00000000000..70dd5d0d9d1 --- /dev/null +++ b/app/assets/images/emoji/cowboy.png diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png Binary files differnew file mode 100644 index 00000000000..19f3047ab61 --- /dev/null +++ b/app/assets/images/emoji/crab.png diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png Binary files differnew file mode 100644 index 00000000000..8d7b427aaa3 --- /dev/null +++ b/app/assets/images/emoji/crayon.png diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png Binary files differnew file mode 100644 index 00000000000..372777d5c61 --- /dev/null +++ b/app/assets/images/emoji/credit_card.png diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png Binary files differnew file mode 100644 index 00000000000..765420ecec7 --- /dev/null +++ b/app/assets/images/emoji/crescent_moon.png diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png Binary files differnew file mode 100644 index 00000000000..d602294a2cd --- /dev/null +++ b/app/assets/images/emoji/cricket.png diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png Binary files differnew file mode 100644 index 00000000000..3005c46f176 --- /dev/null +++ b/app/assets/images/emoji/crocodile.png diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png Binary files differnew file mode 100644 index 00000000000..fb33feb1a38 --- /dev/null +++ b/app/assets/images/emoji/croissant.png diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png Binary files differnew file mode 100644 index 00000000000..42b10e82257 --- /dev/null +++ b/app/assets/images/emoji/cross.png diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png Binary files differnew file mode 100644 index 00000000000..273bd0f0fe5 --- /dev/null +++ b/app/assets/images/emoji/crossed_flags.png diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png Binary files differnew file mode 100644 index 00000000000..907e9607134 --- /dev/null +++ b/app/assets/images/emoji/crossed_swords.png diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png Binary files differnew file mode 100644 index 00000000000..93b82d92f04 --- /dev/null +++ b/app/assets/images/emoji/crown.png diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png Binary files differnew file mode 100644 index 00000000000..19d4acbe40c --- /dev/null +++ b/app/assets/images/emoji/cruise_ship.png diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png Binary files differnew file mode 100644 index 00000000000..b7877f8a173 --- /dev/null +++ b/app/assets/images/emoji/cry.png diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png Binary files differnew file mode 100644 index 00000000000..b4f49715e00 --- /dev/null +++ b/app/assets/images/emoji/crying_cat_face.png diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png Binary files differnew file mode 100644 index 00000000000..485d5c888f1 --- /dev/null +++ b/app/assets/images/emoji/crystal_ball.png diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png Binary files differnew file mode 100644 index 00000000000..500807059d2 --- /dev/null +++ b/app/assets/images/emoji/cucumber.png diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png Binary files differnew file mode 100644 index 00000000000..2df0078ddd1 --- /dev/null +++ b/app/assets/images/emoji/cupid.png diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png Binary files differnew file mode 100644 index 00000000000..440aa56d50e --- /dev/null +++ b/app/assets/images/emoji/curly_loop.png diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png Binary files differnew file mode 100644 index 00000000000..4d46c6050e7 --- /dev/null +++ b/app/assets/images/emoji/currency_exchange.png diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png Binary files differnew file mode 100644 index 00000000000..69657ca8103 --- /dev/null +++ b/app/assets/images/emoji/curry.png diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png Binary files differnew file mode 100644 index 00000000000..fa3df67b8f6 --- /dev/null +++ b/app/assets/images/emoji/custard.png diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png Binary files differnew file mode 100644 index 00000000000..21b7ce2c69e --- /dev/null +++ b/app/assets/images/emoji/customs.png diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png Binary files differnew file mode 100644 index 00000000000..ff00b1afe70 --- /dev/null +++ b/app/assets/images/emoji/cyclone.png diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png Binary files differnew file mode 100644 index 00000000000..66e97b0aa25 --- /dev/null +++ b/app/assets/images/emoji/dagger.png diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png Binary files differnew file mode 100644 index 00000000000..04b166991cb --- /dev/null +++ b/app/assets/images/emoji/dancer.png diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png Binary files differnew file mode 100644 index 00000000000..2c7b11c3a6e --- /dev/null +++ b/app/assets/images/emoji/dancer_tone1.png diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png Binary files differnew file mode 100644 index 00000000000..cb04b1f907e --- /dev/null +++ b/app/assets/images/emoji/dancer_tone2.png diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png Binary files differnew file mode 100644 index 00000000000..98c5bca7b64 --- /dev/null +++ b/app/assets/images/emoji/dancer_tone3.png diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png Binary files differnew file mode 100644 index 00000000000..fdb1e00cbba --- /dev/null +++ b/app/assets/images/emoji/dancer_tone4.png diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png Binary files differnew file mode 100644 index 00000000000..0e34e0e23f0 --- /dev/null +++ b/app/assets/images/emoji/dancer_tone5.png diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png Binary files differnew file mode 100644 index 00000000000..67e6ffacb76 --- /dev/null +++ b/app/assets/images/emoji/dancers.png diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png Binary files differnew file mode 100644 index 00000000000..f73f37b01c7 --- /dev/null +++ b/app/assets/images/emoji/dango.png diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png Binary files differnew file mode 100644 index 00000000000..b1b6db0acff --- /dev/null +++ b/app/assets/images/emoji/dark_sunglasses.png diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png Binary files differnew file mode 100644 index 00000000000..f6704aeb8ba --- /dev/null +++ b/app/assets/images/emoji/dart.png diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png Binary files differnew file mode 100644 index 00000000000..064b8525c12 --- /dev/null +++ b/app/assets/images/emoji/dash.png diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png Binary files differnew file mode 100644 index 00000000000..f05b3da97b8 --- /dev/null +++ b/app/assets/images/emoji/date.png diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png Binary files differnew file mode 100644 index 00000000000..785fc1c30ea --- /dev/null +++ b/app/assets/images/emoji/deciduous_tree.png diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png Binary files differnew file mode 100644 index 00000000000..d8698195ff0 --- /dev/null +++ b/app/assets/images/emoji/deer.png diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png Binary files differnew file mode 100644 index 00000000000..58867c7a6e1 --- /dev/null +++ b/app/assets/images/emoji/department_store.png diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png Binary files differnew file mode 100644 index 00000000000..e9966ff8c65 --- /dev/null +++ b/app/assets/images/emoji/desert.png diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png Binary files differnew file mode 100644 index 00000000000..909bd42b5e1 --- /dev/null +++ b/app/assets/images/emoji/desktop.png diff --git a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png Binary files differnew file mode 100644 index 00000000000..2a22a26d1e2 --- /dev/null +++ b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png Binary files differnew file mode 100644 index 00000000000..1f25f51f97a --- /dev/null +++ b/app/assets/images/emoji/diamonds.png diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png Binary files differnew file mode 100644 index 00000000000..efe4e67e23c --- /dev/null +++ b/app/assets/images/emoji/disappointed.png diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png Binary files differnew file mode 100644 index 00000000000..aef864d2b3d --- /dev/null +++ b/app/assets/images/emoji/disappointed_relieved.png diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png Binary files differnew file mode 100644 index 00000000000..46a7e403f9d --- /dev/null +++ b/app/assets/images/emoji/dividers.png diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png Binary files differnew file mode 100644 index 00000000000..85f52efad24 --- /dev/null +++ b/app/assets/images/emoji/dizzy.png diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png Binary files differnew file mode 100644 index 00000000000..3120316ab5e --- /dev/null +++ b/app/assets/images/emoji/dizzy_face.png diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png Binary files differnew file mode 100644 index 00000000000..341d2575f4f --- /dev/null +++ b/app/assets/images/emoji/do_not_litter.png diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png Binary files differnew file mode 100644 index 00000000000..281b81d58bd --- /dev/null +++ b/app/assets/images/emoji/dog.png diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png Binary files differnew file mode 100644 index 00000000000..976143dbdbe --- /dev/null +++ b/app/assets/images/emoji/dog2.png diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png Binary files differnew file mode 100644 index 00000000000..a9904c28293 --- /dev/null +++ b/app/assets/images/emoji/dollar.png diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png Binary files differnew file mode 100644 index 00000000000..10955615110 --- /dev/null +++ b/app/assets/images/emoji/dolls.png diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png Binary files differnew file mode 100644 index 00000000000..81434809003 --- /dev/null +++ b/app/assets/images/emoji/dolphin.png diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png Binary files differnew file mode 100644 index 00000000000..36ae3e27494 --- /dev/null +++ b/app/assets/images/emoji/door.png diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png Binary files differnew file mode 100644 index 00000000000..0ca4cd0bde8 --- /dev/null +++ b/app/assets/images/emoji/doughnut.png diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png Binary files differnew file mode 100644 index 00000000000..9580c4917d7 --- /dev/null +++ b/app/assets/images/emoji/dove.png diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png Binary files differnew file mode 100644 index 00000000000..d6311cf5429 --- /dev/null +++ b/app/assets/images/emoji/dragon.png diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png Binary files differnew file mode 100644 index 00000000000..3c2720446c6 --- /dev/null +++ b/app/assets/images/emoji/dragon_face.png diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png Binary files differnew file mode 100644 index 00000000000..a697ca5c57d --- /dev/null +++ b/app/assets/images/emoji/dress.png diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png Binary files differnew file mode 100644 index 00000000000..5271637c7c4 --- /dev/null +++ b/app/assets/images/emoji/dromedary_camel.png diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png Binary files differnew file mode 100644 index 00000000000..a5460532597 --- /dev/null +++ b/app/assets/images/emoji/drooling_face.png diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png Binary files differnew file mode 100644 index 00000000000..71241ec3061 --- /dev/null +++ b/app/assets/images/emoji/droplet.png diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png Binary files differnew file mode 100644 index 00000000000..b038727cc99 --- /dev/null +++ b/app/assets/images/emoji/drum.png diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png Binary files differnew file mode 100644 index 00000000000..74330b77ca3 --- /dev/null +++ b/app/assets/images/emoji/duck.png diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png Binary files differnew file mode 100644 index 00000000000..045a6f7a08d --- /dev/null +++ b/app/assets/images/emoji/dvd.png diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png Binary files differnew file mode 100644 index 00000000000..d22e654a20b --- /dev/null +++ b/app/assets/images/emoji/e-mail.png diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png Binary files differnew file mode 100644 index 00000000000..4f277debeef --- /dev/null +++ b/app/assets/images/emoji/eagle.png diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png Binary files differnew file mode 100644 index 00000000000..f84f9ff154a --- /dev/null +++ b/app/assets/images/emoji/ear.png diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png Binary files differnew file mode 100644 index 00000000000..3564d9d643a --- /dev/null +++ b/app/assets/images/emoji/ear_of_rice.png diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png Binary files differnew file mode 100644 index 00000000000..d09e1e41996 --- /dev/null +++ b/app/assets/images/emoji/ear_tone1.png diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png Binary files differnew file mode 100644 index 00000000000..300d60a9948 --- /dev/null +++ b/app/assets/images/emoji/ear_tone2.png diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png Binary files differnew file mode 100644 index 00000000000..2a56eebe445 --- /dev/null +++ b/app/assets/images/emoji/ear_tone3.png diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png Binary files differnew file mode 100644 index 00000000000..bd270f7763e --- /dev/null +++ b/app/assets/images/emoji/ear_tone4.png diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png Binary files differnew file mode 100644 index 00000000000..b96bb441dff --- /dev/null +++ b/app/assets/images/emoji/ear_tone5.png diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png Binary files differnew file mode 100644 index 00000000000..66c3348c23a --- /dev/null +++ b/app/assets/images/emoji/earth_africa.png diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png Binary files differnew file mode 100644 index 00000000000..538c3cddd68 --- /dev/null +++ b/app/assets/images/emoji/earth_americas.png diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png Binary files differnew file mode 100644 index 00000000000..d8df97fec3c --- /dev/null +++ b/app/assets/images/emoji/earth_asia.png diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png Binary files differnew file mode 100644 index 00000000000..c171974d993 --- /dev/null +++ b/app/assets/images/emoji/egg.png diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png Binary files differnew file mode 100644 index 00000000000..fafd7c1a14c --- /dev/null +++ b/app/assets/images/emoji/eggplant.png diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png Binary files differnew file mode 100644 index 00000000000..8c95874d4c5 --- /dev/null +++ b/app/assets/images/emoji/eight.png diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png Binary files differnew file mode 100644 index 00000000000..820179bda50 --- /dev/null +++ b/app/assets/images/emoji/eight_pointed_black_star.png diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png Binary files differnew file mode 100644 index 00000000000..3307ffa62ee --- /dev/null +++ b/app/assets/images/emoji/eight_spoked_asterisk.png diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png Binary files differnew file mode 100644 index 00000000000..ec5cfc48973 --- /dev/null +++ b/app/assets/images/emoji/eject.png diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png Binary files differnew file mode 100644 index 00000000000..31d1eb215b4 --- /dev/null +++ b/app/assets/images/emoji/electric_plug.png diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png Binary files differnew file mode 100644 index 00000000000..b8a6d140595 --- /dev/null +++ b/app/assets/images/emoji/elephant.png diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png Binary files differnew file mode 100644 index 00000000000..ef3ccd5f367 --- /dev/null +++ b/app/assets/images/emoji/end.png diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png Binary files differnew file mode 100644 index 00000000000..ec77ac375a4 --- /dev/null +++ b/app/assets/images/emoji/envelope.png diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png Binary files differnew file mode 100644 index 00000000000..7448a6b7673 --- /dev/null +++ b/app/assets/images/emoji/envelope_with_arrow.png diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png Binary files differnew file mode 100644 index 00000000000..a49020820e1 --- /dev/null +++ b/app/assets/images/emoji/euro.png diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png Binary files differnew file mode 100644 index 00000000000..888d11332ce --- /dev/null +++ b/app/assets/images/emoji/european_castle.png diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png Binary files differnew file mode 100644 index 00000000000..3745aff8dd2 --- /dev/null +++ b/app/assets/images/emoji/european_post_office.png diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png Binary files differnew file mode 100644 index 00000000000..f679d8dd772 --- /dev/null +++ b/app/assets/images/emoji/evergreen_tree.png diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png Binary files differnew file mode 100644 index 00000000000..2c14406422f --- /dev/null +++ b/app/assets/images/emoji/exclamation.png diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png Binary files differnew file mode 100644 index 00000000000..2954017f6c2 --- /dev/null +++ b/app/assets/images/emoji/expressionless.png diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png Binary files differnew file mode 100644 index 00000000000..9d989cdd375 --- /dev/null +++ b/app/assets/images/emoji/eye.png diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png Binary files differnew file mode 100644 index 00000000000..21bd22bbcce --- /dev/null +++ b/app/assets/images/emoji/eye_in_speech_bubble.png diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png Binary files differnew file mode 100644 index 00000000000..865d8274acf --- /dev/null +++ b/app/assets/images/emoji/eyeglasses.png diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png Binary files differnew file mode 100644 index 00000000000..2102ada7e09 --- /dev/null +++ b/app/assets/images/emoji/eyes.png diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png Binary files differnew file mode 100644 index 00000000000..defc796cf16 --- /dev/null +++ b/app/assets/images/emoji/face_palm.png diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png Binary files differnew file mode 100644 index 00000000000..2f4b010bb40 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone1.png diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png Binary files differnew file mode 100644 index 00000000000..97fb6831687 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone2.png diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png Binary files differnew file mode 100644 index 00000000000..b5b5c1e5306 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone3.png diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png Binary files differnew file mode 100644 index 00000000000..2840b113483 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone4.png diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png Binary files differnew file mode 100644 index 00000000000..6f070db98be --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone5.png diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png Binary files differnew file mode 100644 index 00000000000..e1d2ddf4a27 --- /dev/null +++ b/app/assets/images/emoji/factory.png diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png Binary files differnew file mode 100644 index 00000000000..0d60e7bdf2d --- /dev/null +++ b/app/assets/images/emoji/fallen_leaf.png diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png Binary files differnew file mode 100644 index 00000000000..26421965791 --- /dev/null +++ b/app/assets/images/emoji/family.png diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png Binary files differnew file mode 100644 index 00000000000..7a2e4e2c491 --- /dev/null +++ b/app/assets/images/emoji/family_mmb.png diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png Binary files differnew file mode 100644 index 00000000000..81e6c0fc0ee --- /dev/null +++ b/app/assets/images/emoji/family_mmbb.png diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png Binary files differnew file mode 100644 index 00000000000..932a85e1fe5 --- /dev/null +++ b/app/assets/images/emoji/family_mmg.png diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png Binary files differnew file mode 100644 index 00000000000..41e35166670 --- /dev/null +++ b/app/assets/images/emoji/family_mmgb.png diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png Binary files differnew file mode 100644 index 00000000000..8e8ccfe6c7f --- /dev/null +++ b/app/assets/images/emoji/family_mmgg.png diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png Binary files differnew file mode 100644 index 00000000000..b544fbe573f --- /dev/null +++ b/app/assets/images/emoji/family_mwbb.png diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png Binary files differnew file mode 100644 index 00000000000..71d2681c32a --- /dev/null +++ b/app/assets/images/emoji/family_mwg.png diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png Binary files differnew file mode 100644 index 00000000000..40dbf1f7a18 --- /dev/null +++ b/app/assets/images/emoji/family_mwgb.png diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png Binary files differnew file mode 100644 index 00000000000..bfefa4879cb --- /dev/null +++ b/app/assets/images/emoji/family_mwgg.png diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png Binary files differnew file mode 100644 index 00000000000..836feae7c78 --- /dev/null +++ b/app/assets/images/emoji/family_wwb.png diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png Binary files differnew file mode 100644 index 00000000000..6c6ba45e7bb --- /dev/null +++ b/app/assets/images/emoji/family_wwbb.png diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png Binary files differnew file mode 100644 index 00000000000..41225c6fa5a --- /dev/null +++ b/app/assets/images/emoji/family_wwg.png diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png Binary files differnew file mode 100644 index 00000000000..284d29ab5da --- /dev/null +++ b/app/assets/images/emoji/family_wwgb.png diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png Binary files differnew file mode 100644 index 00000000000..d8d3f49b85f --- /dev/null +++ b/app/assets/images/emoji/family_wwgg.png diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png Binary files differnew file mode 100644 index 00000000000..c406fedfdb1 --- /dev/null +++ b/app/assets/images/emoji/fast_forward.png diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png Binary files differnew file mode 100644 index 00000000000..6f929e294c2 --- /dev/null +++ b/app/assets/images/emoji/fax.png diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png Binary files differnew file mode 100644 index 00000000000..eb8b347cef9 --- /dev/null +++ b/app/assets/images/emoji/fearful.png diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png Binary files differnew file mode 100644 index 00000000000..5fe568cee93 --- /dev/null +++ b/app/assets/images/emoji/feet.png diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png Binary files differnew file mode 100644 index 00000000000..5288c920eb9 --- /dev/null +++ b/app/assets/images/emoji/fencer.png diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png Binary files differnew file mode 100644 index 00000000000..55c8ff0475b --- /dev/null +++ b/app/assets/images/emoji/ferris_wheel.png diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png Binary files differnew file mode 100644 index 00000000000..41816b3ae34 --- /dev/null +++ b/app/assets/images/emoji/ferry.png diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png Binary files differnew file mode 100644 index 00000000000..839637716ee --- /dev/null +++ b/app/assets/images/emoji/field_hockey.png diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png Binary files differnew file mode 100644 index 00000000000..fddc65dde96 --- /dev/null +++ b/app/assets/images/emoji/file_cabinet.png diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png Binary files differnew file mode 100644 index 00000000000..addedaf0870 --- /dev/null +++ b/app/assets/images/emoji/file_folder.png diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png Binary files differnew file mode 100644 index 00000000000..30143aedbe6 --- /dev/null +++ b/app/assets/images/emoji/film_frames.png diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png Binary files differnew file mode 100644 index 00000000000..4cd18514ea3 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed.png diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png Binary files differnew file mode 100644 index 00000000000..dd2384a6cd5 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone1.png diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png Binary files differnew file mode 100644 index 00000000000..6228401befe --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone2.png diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png Binary files differnew file mode 100644 index 00000000000..b1074da15f5 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone3.png diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png Binary files differnew file mode 100644 index 00000000000..75e05e4d332 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone4.png diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png Binary files differnew file mode 100644 index 00000000000..761aebdc30f --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone5.png diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png Binary files differnew file mode 100644 index 00000000000..bd3775a460b --- /dev/null +++ b/app/assets/images/emoji/fire.png diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png Binary files differnew file mode 100644 index 00000000000..2cd45b7cf7e --- /dev/null +++ b/app/assets/images/emoji/fire_engine.png diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png Binary files differnew file mode 100644 index 00000000000..176c8b58265 --- /dev/null +++ b/app/assets/images/emoji/fireworks.png diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png Binary files differnew file mode 100644 index 00000000000..15612b66492 --- /dev/null +++ b/app/assets/images/emoji/first_place.png diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png Binary files differnew file mode 100644 index 00000000000..5dccaf72a4f --- /dev/null +++ b/app/assets/images/emoji/first_quarter_moon.png diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..cd8a3d7acd8 --- /dev/null +++ b/app/assets/images/emoji/first_quarter_moon_with_face.png diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png Binary files differnew file mode 100644 index 00000000000..c2d2faaacd4 --- /dev/null +++ b/app/assets/images/emoji/fish.png diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png Binary files differnew file mode 100644 index 00000000000..157bded65db --- /dev/null +++ b/app/assets/images/emoji/fish_cake.png diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png Binary files differnew file mode 100644 index 00000000000..dfcdf07eb50 --- /dev/null +++ b/app/assets/images/emoji/fishing_pole_and_fish.png diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png Binary files differnew file mode 100644 index 00000000000..de33592bf98 --- /dev/null +++ b/app/assets/images/emoji/fist.png diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png Binary files differnew file mode 100644 index 00000000000..02809e2dd68 --- /dev/null +++ b/app/assets/images/emoji/fist_tone1.png diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png Binary files differnew file mode 100644 index 00000000000..5de34810383 --- /dev/null +++ b/app/assets/images/emoji/fist_tone2.png diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png Binary files differnew file mode 100644 index 00000000000..0d5240129b1 --- /dev/null +++ b/app/assets/images/emoji/fist_tone3.png diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png Binary files differnew file mode 100644 index 00000000000..a95c0dd634b --- /dev/null +++ b/app/assets/images/emoji/fist_tone4.png diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png Binary files differnew file mode 100644 index 00000000000..a2f092fd8c7 --- /dev/null +++ b/app/assets/images/emoji/fist_tone5.png diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png Binary files differnew file mode 100644 index 00000000000..d14371f3f27 --- /dev/null +++ b/app/assets/images/emoji/five.png diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png Binary files differnew file mode 100644 index 00000000000..286239920c7 --- /dev/null +++ b/app/assets/images/emoji/flag_ac.png diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png Binary files differnew file mode 100644 index 00000000000..20f4b14e8ad --- /dev/null +++ b/app/assets/images/emoji/flag_ad.png diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png Binary files differnew file mode 100644 index 00000000000..d16ffe4b862 --- /dev/null +++ b/app/assets/images/emoji/flag_ae.png diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png Binary files differnew file mode 100644 index 00000000000..a51533b554d --- /dev/null +++ b/app/assets/images/emoji/flag_af.png diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png Binary files differnew file mode 100644 index 00000000000..07f2ce397d0 --- /dev/null +++ b/app/assets/images/emoji/flag_ag.png diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png Binary files differnew file mode 100644 index 00000000000..500b5ab09fb --- /dev/null +++ b/app/assets/images/emoji/flag_ai.png diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png Binary files differnew file mode 100644 index 00000000000..03a20132cc6 --- /dev/null +++ b/app/assets/images/emoji/flag_al.png diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png Binary files differnew file mode 100644 index 00000000000..2ad60a273ec --- /dev/null +++ b/app/assets/images/emoji/flag_am.png diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png Binary files differnew file mode 100644 index 00000000000..cb46c31f862 --- /dev/null +++ b/app/assets/images/emoji/flag_ao.png diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png Binary files differnew file mode 100644 index 00000000000..b272021d375 --- /dev/null +++ b/app/assets/images/emoji/flag_aq.png diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png Binary files differnew file mode 100644 index 00000000000..73136caf3b7 --- /dev/null +++ b/app/assets/images/emoji/flag_ar.png diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png Binary files differnew file mode 100644 index 00000000000..3db45a0d9f3 --- /dev/null +++ b/app/assets/images/emoji/flag_as.png diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png Binary files differnew file mode 100644 index 00000000000..c43769dcb19 --- /dev/null +++ b/app/assets/images/emoji/flag_at.png diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png Binary files differnew file mode 100644 index 00000000000..7794309c78c --- /dev/null +++ b/app/assets/images/emoji/flag_au.png diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png Binary files differnew file mode 100644 index 00000000000..02c840d12c9 --- /dev/null +++ b/app/assets/images/emoji/flag_aw.png diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png Binary files differnew file mode 100644 index 00000000000..fc5466174bb --- /dev/null +++ b/app/assets/images/emoji/flag_ax.png diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png Binary files differnew file mode 100644 index 00000000000..89d3d15fd9f --- /dev/null +++ b/app/assets/images/emoji/flag_az.png diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png Binary files differnew file mode 100644 index 00000000000..25fe407e13c --- /dev/null +++ b/app/assets/images/emoji/flag_ba.png diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png Binary files differnew file mode 100644 index 00000000000..bccd8c5c9b0 --- /dev/null +++ b/app/assets/images/emoji/flag_bb.png diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png Binary files differnew file mode 100644 index 00000000000..b0597a3149b --- /dev/null +++ b/app/assets/images/emoji/flag_bd.png diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png Binary files differnew file mode 100644 index 00000000000..551f086e3c4 --- /dev/null +++ b/app/assets/images/emoji/flag_be.png diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png Binary files differnew file mode 100644 index 00000000000..444d4829f94 --- /dev/null +++ b/app/assets/images/emoji/flag_bf.png diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png Binary files differnew file mode 100644 index 00000000000..821eee5e170 --- /dev/null +++ b/app/assets/images/emoji/flag_bg.png diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png Binary files differnew file mode 100644 index 00000000000..f33724249f0 --- /dev/null +++ b/app/assets/images/emoji/flag_bh.png diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png Binary files differnew file mode 100644 index 00000000000..ea20ac93211 --- /dev/null +++ b/app/assets/images/emoji/flag_bi.png diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png Binary files differnew file mode 100644 index 00000000000..7cca4f80457 --- /dev/null +++ b/app/assets/images/emoji/flag_bj.png diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png Binary files differnew file mode 100644 index 00000000000..1082e78999f --- /dev/null +++ b/app/assets/images/emoji/flag_bl.png diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png Binary files differnew file mode 100644 index 00000000000..0e28d05d5ac --- /dev/null +++ b/app/assets/images/emoji/flag_black.png diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png Binary files differnew file mode 100644 index 00000000000..ab8cafdac63 --- /dev/null +++ b/app/assets/images/emoji/flag_bm.png diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png Binary files differnew file mode 100644 index 00000000000..caa9329a896 --- /dev/null +++ b/app/assets/images/emoji/flag_bn.png diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png Binary files differnew file mode 100644 index 00000000000..98af62b3da7 --- /dev/null +++ b/app/assets/images/emoji/flag_bo.png diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png Binary files differnew file mode 100644 index 00000000000..cb978ef9de9 --- /dev/null +++ b/app/assets/images/emoji/flag_bq.png diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png Binary files differnew file mode 100644 index 00000000000..b139366a42b --- /dev/null +++ b/app/assets/images/emoji/flag_br.png diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png Binary files differnew file mode 100644 index 00000000000..d36bcd2fb52 --- /dev/null +++ b/app/assets/images/emoji/flag_bs.png diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png Binary files differnew file mode 100644 index 00000000000..ed57aa0360e --- /dev/null +++ b/app/assets/images/emoji/flag_bt.png diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png Binary files differnew file mode 100644 index 00000000000..5884e648228 --- /dev/null +++ b/app/assets/images/emoji/flag_bv.png diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png Binary files differnew file mode 100644 index 00000000000..cb12f34739d --- /dev/null +++ b/app/assets/images/emoji/flag_bw.png diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png Binary files differnew file mode 100644 index 00000000000..859c05beb13 --- /dev/null +++ b/app/assets/images/emoji/flag_by.png diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png Binary files differnew file mode 100644 index 00000000000..34761cd03d8 --- /dev/null +++ b/app/assets/images/emoji/flag_bz.png diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png Binary files differnew file mode 100644 index 00000000000..7c5b390e85b --- /dev/null +++ b/app/assets/images/emoji/flag_ca.png diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png Binary files differnew file mode 100644 index 00000000000..b6555a23d83 --- /dev/null +++ b/app/assets/images/emoji/flag_cc.png diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png Binary files differnew file mode 100644 index 00000000000..fa92009771d --- /dev/null +++ b/app/assets/images/emoji/flag_cd.png diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png Binary files differnew file mode 100644 index 00000000000..b969ae29ea9 --- /dev/null +++ b/app/assets/images/emoji/flag_cf.png diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png Binary files differnew file mode 100644 index 00000000000..3a38a40a95e --- /dev/null +++ b/app/assets/images/emoji/flag_cg.png diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png Binary files differnew file mode 100644 index 00000000000..5ff86b8a3b7 --- /dev/null +++ b/app/assets/images/emoji/flag_ch.png diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png Binary files differnew file mode 100644 index 00000000000..e3b4d15c7f1 --- /dev/null +++ b/app/assets/images/emoji/flag_ci.png diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png Binary files differnew file mode 100644 index 00000000000..b6b53dbc1c4 --- /dev/null +++ b/app/assets/images/emoji/flag_ck.png diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png Binary files differnew file mode 100644 index 00000000000..c9390da5499 --- /dev/null +++ b/app/assets/images/emoji/flag_cl.png diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png Binary files differnew file mode 100644 index 00000000000..2d3f6ec4518 --- /dev/null +++ b/app/assets/images/emoji/flag_cm.png diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png Binary files differnew file mode 100644 index 00000000000..0a7f350a6d2 --- /dev/null +++ b/app/assets/images/emoji/flag_cn.png diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png Binary files differnew file mode 100644 index 00000000000..7e0f5e0dc3c --- /dev/null +++ b/app/assets/images/emoji/flag_co.png diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_cp.png diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png Binary files differnew file mode 100644 index 00000000000..a5fce126515 --- /dev/null +++ b/app/assets/images/emoji/flag_cr.png diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png Binary files differnew file mode 100644 index 00000000000..447328f7dfd --- /dev/null +++ b/app/assets/images/emoji/flag_cu.png diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png Binary files differnew file mode 100644 index 00000000000..43faf4d64d5 --- /dev/null +++ b/app/assets/images/emoji/flag_cv.png diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png Binary files differnew file mode 100644 index 00000000000..eb39e8d0078 --- /dev/null +++ b/app/assets/images/emoji/flag_cw.png diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png Binary files differnew file mode 100644 index 00000000000..09d21359f3a --- /dev/null +++ b/app/assets/images/emoji/flag_cx.png diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png Binary files differnew file mode 100644 index 00000000000..154a7aa3176 --- /dev/null +++ b/app/assets/images/emoji/flag_cy.png diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png Binary files differnew file mode 100644 index 00000000000..9737ca223c7 --- /dev/null +++ b/app/assets/images/emoji/flag_cz.png diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png Binary files differnew file mode 100644 index 00000000000..98ed76b3bab --- /dev/null +++ b/app/assets/images/emoji/flag_de.png diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png Binary files differnew file mode 100644 index 00000000000..aae927d14b8 --- /dev/null +++ b/app/assets/images/emoji/flag_dg.png diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png Binary files differnew file mode 100644 index 00000000000..73c2a2acbd9 --- /dev/null +++ b/app/assets/images/emoji/flag_dj.png diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png Binary files differnew file mode 100644 index 00000000000..e5a60b06256 --- /dev/null +++ b/app/assets/images/emoji/flag_dk.png diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png Binary files differnew file mode 100644 index 00000000000..50f8a53981d --- /dev/null +++ b/app/assets/images/emoji/flag_dm.png diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png Binary files differnew file mode 100644 index 00000000000..037a45d7c26 --- /dev/null +++ b/app/assets/images/emoji/flag_do.png diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png Binary files differnew file mode 100644 index 00000000000..24945b10f2d --- /dev/null +++ b/app/assets/images/emoji/flag_dz.png diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png Binary files differnew file mode 100644 index 00000000000..356ff347838 --- /dev/null +++ b/app/assets/images/emoji/flag_ea.png diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png Binary files differnew file mode 100644 index 00000000000..13814594619 --- /dev/null +++ b/app/assets/images/emoji/flag_ec.png diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png Binary files differnew file mode 100644 index 00000000000..84f317e7747 --- /dev/null +++ b/app/assets/images/emoji/flag_ee.png diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png Binary files differnew file mode 100644 index 00000000000..57786064a95 --- /dev/null +++ b/app/assets/images/emoji/flag_eg.png diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png Binary files differnew file mode 100644 index 00000000000..4d7a76687f6 --- /dev/null +++ b/app/assets/images/emoji/flag_eh.png diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png Binary files differnew file mode 100644 index 00000000000..0c3c724c1fb --- /dev/null +++ b/app/assets/images/emoji/flag_er.png diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png Binary files differnew file mode 100644 index 00000000000..3e73597a225 --- /dev/null +++ b/app/assets/images/emoji/flag_es.png diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png Binary files differnew file mode 100644 index 00000000000..9560a134c97 --- /dev/null +++ b/app/assets/images/emoji/flag_et.png diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png Binary files differnew file mode 100644 index 00000000000..0b456cf3330 --- /dev/null +++ b/app/assets/images/emoji/flag_eu.png diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png Binary files differnew file mode 100644 index 00000000000..ebcf58abfc5 --- /dev/null +++ b/app/assets/images/emoji/flag_fi.png diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png Binary files differnew file mode 100644 index 00000000000..9cc8c37fe37 --- /dev/null +++ b/app/assets/images/emoji/flag_fj.png diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png Binary files differnew file mode 100644 index 00000000000..61372fd2549 --- /dev/null +++ b/app/assets/images/emoji/flag_fk.png diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png Binary files differnew file mode 100644 index 00000000000..0889825c8e1 --- /dev/null +++ b/app/assets/images/emoji/flag_fm.png diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png Binary files differnew file mode 100644 index 00000000000..9a4431b0831 --- /dev/null +++ b/app/assets/images/emoji/flag_fo.png diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png Binary files differnew file mode 100644 index 00000000000..62ca19c3fcf --- /dev/null +++ b/app/assets/images/emoji/flag_fr.png diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png Binary files differnew file mode 100644 index 00000000000..2e68e527a3e --- /dev/null +++ b/app/assets/images/emoji/flag_ga.png diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png Binary files differnew file mode 100644 index 00000000000..3ed10f62347 --- /dev/null +++ b/app/assets/images/emoji/flag_gb.png diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png Binary files differnew file mode 100644 index 00000000000..527aad33807 --- /dev/null +++ b/app/assets/images/emoji/flag_gd.png diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png Binary files differnew file mode 100644 index 00000000000..a75d142480d --- /dev/null +++ b/app/assets/images/emoji/flag_ge.png diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png Binary files differnew file mode 100644 index 00000000000..0cf96f327c0 --- /dev/null +++ b/app/assets/images/emoji/flag_gf.png diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png Binary files differnew file mode 100644 index 00000000000..970002c7f76 --- /dev/null +++ b/app/assets/images/emoji/flag_gg.png diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png Binary files differnew file mode 100644 index 00000000000..f31b5eb7b45 --- /dev/null +++ b/app/assets/images/emoji/flag_gh.png diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png Binary files differnew file mode 100644 index 00000000000..e554a2a1d0c --- /dev/null +++ b/app/assets/images/emoji/flag_gi.png diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png Binary files differnew file mode 100644 index 00000000000..2e795dd4e33 --- /dev/null +++ b/app/assets/images/emoji/flag_gl.png diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png Binary files differnew file mode 100644 index 00000000000..bb69c0975a3 --- /dev/null +++ b/app/assets/images/emoji/flag_gm.png diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png Binary files differnew file mode 100644 index 00000000000..1981f61dbf5 --- /dev/null +++ b/app/assets/images/emoji/flag_gn.png diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png Binary files differnew file mode 100644 index 00000000000..10e42e672bd --- /dev/null +++ b/app/assets/images/emoji/flag_gp.png diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png Binary files differnew file mode 100644 index 00000000000..11475e61eeb --- /dev/null +++ b/app/assets/images/emoji/flag_gq.png diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png Binary files differnew file mode 100644 index 00000000000..0f6bb1b6b94 --- /dev/null +++ b/app/assets/images/emoji/flag_gr.png diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png Binary files differnew file mode 100644 index 00000000000..6fc92780453 --- /dev/null +++ b/app/assets/images/emoji/flag_gs.png diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png Binary files differnew file mode 100644 index 00000000000..7213d4139ed --- /dev/null +++ b/app/assets/images/emoji/flag_gt.png diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png Binary files differnew file mode 100644 index 00000000000..4027549ca3c --- /dev/null +++ b/app/assets/images/emoji/flag_gu.png diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png Binary files differnew file mode 100644 index 00000000000..6357f6225f4 --- /dev/null +++ b/app/assets/images/emoji/flag_gw.png diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png Binary files differnew file mode 100644 index 00000000000..746e2fb7e44 --- /dev/null +++ b/app/assets/images/emoji/flag_gy.png diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png Binary files differnew file mode 100644 index 00000000000..cf0c7151b56 --- /dev/null +++ b/app/assets/images/emoji/flag_hk.png diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png Binary files differnew file mode 100644 index 00000000000..b613509e466 --- /dev/null +++ b/app/assets/images/emoji/flag_hm.png diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png Binary files differnew file mode 100644 index 00000000000..402cdcefdf8 --- /dev/null +++ b/app/assets/images/emoji/flag_hn.png diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png Binary files differnew file mode 100644 index 00000000000..46f4f06b4f2 --- /dev/null +++ b/app/assets/images/emoji/flag_hr.png diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png Binary files differnew file mode 100644 index 00000000000..d8d0c888498 --- /dev/null +++ b/app/assets/images/emoji/flag_ht.png diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png Binary files differnew file mode 100644 index 00000000000..a898de636a5 --- /dev/null +++ b/app/assets/images/emoji/flag_hu.png diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png Binary files differnew file mode 100644 index 00000000000..69fd990aa95 --- /dev/null +++ b/app/assets/images/emoji/flag_ic.png diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png Binary files differnew file mode 100644 index 00000000000..85b4c063a45 --- /dev/null +++ b/app/assets/images/emoji/flag_id.png diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png Binary files differnew file mode 100644 index 00000000000..a28295838cc --- /dev/null +++ b/app/assets/images/emoji/flag_ie.png diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png Binary files differnew file mode 100644 index 00000000000..85c410d45fb --- /dev/null +++ b/app/assets/images/emoji/flag_il.png diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png Binary files differnew file mode 100644 index 00000000000..60a2458e38e --- /dev/null +++ b/app/assets/images/emoji/flag_im.png diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png Binary files differnew file mode 100644 index 00000000000..feccc8952ce --- /dev/null +++ b/app/assets/images/emoji/flag_in.png diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png Binary files differnew file mode 100644 index 00000000000..aae927d14b8 --- /dev/null +++ b/app/assets/images/emoji/flag_io.png diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png Binary files differnew file mode 100644 index 00000000000..41fd1db6f86 --- /dev/null +++ b/app/assets/images/emoji/flag_iq.png diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png Binary files differnew file mode 100644 index 00000000000..ff7aaf62ba6 --- /dev/null +++ b/app/assets/images/emoji/flag_ir.png diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png Binary files differnew file mode 100644 index 00000000000..ad8d4131dd2 --- /dev/null +++ b/app/assets/images/emoji/flag_is.png diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png Binary files differnew file mode 100644 index 00000000000..f21563ec533 --- /dev/null +++ b/app/assets/images/emoji/flag_it.png diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png Binary files differnew file mode 100644 index 00000000000..198a918f6a4 --- /dev/null +++ b/app/assets/images/emoji/flag_je.png diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png Binary files differnew file mode 100644 index 00000000000..f84e4f9e8db --- /dev/null +++ b/app/assets/images/emoji/flag_jm.png diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png Binary files differnew file mode 100644 index 00000000000..20bfa147e3e --- /dev/null +++ b/app/assets/images/emoji/flag_jo.png diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png Binary files differnew file mode 100644 index 00000000000..8d8838e4708 --- /dev/null +++ b/app/assets/images/emoji/flag_jp.png diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png Binary files differnew file mode 100644 index 00000000000..9e417ab3009 --- /dev/null +++ b/app/assets/images/emoji/flag_ke.png diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png Binary files differnew file mode 100644 index 00000000000..2f2d848fe58 --- /dev/null +++ b/app/assets/images/emoji/flag_kg.png diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png Binary files differnew file mode 100644 index 00000000000..9a2877dd620 --- /dev/null +++ b/app/assets/images/emoji/flag_kh.png diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png Binary files differnew file mode 100644 index 00000000000..10e507e3245 --- /dev/null +++ b/app/assets/images/emoji/flag_ki.png diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png Binary files differnew file mode 100644 index 00000000000..bd5a0588e03 --- /dev/null +++ b/app/assets/images/emoji/flag_km.png diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png Binary files differnew file mode 100644 index 00000000000..776207c9605 --- /dev/null +++ b/app/assets/images/emoji/flag_kn.png diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png Binary files differnew file mode 100644 index 00000000000..6b3fd89eaaa --- /dev/null +++ b/app/assets/images/emoji/flag_kp.png diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png Binary files differnew file mode 100644 index 00000000000..833a88116e1 --- /dev/null +++ b/app/assets/images/emoji/flag_kr.png diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png Binary files differnew file mode 100644 index 00000000000..4d19bfa6ca7 --- /dev/null +++ b/app/assets/images/emoji/flag_kw.png diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png Binary files differnew file mode 100644 index 00000000000..40daa4da597 --- /dev/null +++ b/app/assets/images/emoji/flag_ky.png diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png Binary files differnew file mode 100644 index 00000000000..2f97a8fd3c6 --- /dev/null +++ b/app/assets/images/emoji/flag_kz.png diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png Binary files differnew file mode 100644 index 00000000000..4d4179f34f6 --- /dev/null +++ b/app/assets/images/emoji/flag_la.png diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png Binary files differnew file mode 100644 index 00000000000..3d594467011 --- /dev/null +++ b/app/assets/images/emoji/flag_lb.png diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png Binary files differnew file mode 100644 index 00000000000..45547b1e439 --- /dev/null +++ b/app/assets/images/emoji/flag_lc.png diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png Binary files differnew file mode 100644 index 00000000000..0eafa6a2215 --- /dev/null +++ b/app/assets/images/emoji/flag_li.png diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png Binary files differnew file mode 100644 index 00000000000..ab4fe10c40c --- /dev/null +++ b/app/assets/images/emoji/flag_lk.png diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png Binary files differnew file mode 100644 index 00000000000..f66f267fea2 --- /dev/null +++ b/app/assets/images/emoji/flag_lr.png diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png Binary files differnew file mode 100644 index 00000000000..24745631e3c --- /dev/null +++ b/app/assets/images/emoji/flag_ls.png diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png Binary files differnew file mode 100644 index 00000000000..d644b56d62a --- /dev/null +++ b/app/assets/images/emoji/flag_lt.png diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png Binary files differnew file mode 100644 index 00000000000..a2df9c92994 --- /dev/null +++ b/app/assets/images/emoji/flag_lu.png diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png Binary files differnew file mode 100644 index 00000000000..ae680d5f0e3 --- /dev/null +++ b/app/assets/images/emoji/flag_lv.png diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png Binary files differnew file mode 100644 index 00000000000..f6e77b0f3ba --- /dev/null +++ b/app/assets/images/emoji/flag_ly.png diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png Binary files differnew file mode 100644 index 00000000000..c4a056722cd --- /dev/null +++ b/app/assets/images/emoji/flag_ma.png diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png Binary files differnew file mode 100644 index 00000000000..d479eab98cb --- /dev/null +++ b/app/assets/images/emoji/flag_mc.png diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png Binary files differnew file mode 100644 index 00000000000..a7a72539872 --- /dev/null +++ b/app/assets/images/emoji/flag_md.png diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png Binary files differnew file mode 100644 index 00000000000..7c771e7e120 --- /dev/null +++ b/app/assets/images/emoji/flag_me.png diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_mf.png diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png Binary files differnew file mode 100644 index 00000000000..2f3ccdda76f --- /dev/null +++ b/app/assets/images/emoji/flag_mg.png diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png Binary files differnew file mode 100644 index 00000000000..598016481c1 --- /dev/null +++ b/app/assets/images/emoji/flag_mh.png diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png Binary files differnew file mode 100644 index 00000000000..7ba775ee75c --- /dev/null +++ b/app/assets/images/emoji/flag_mk.png diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png Binary files differnew file mode 100644 index 00000000000..68343785468 --- /dev/null +++ b/app/assets/images/emoji/flag_ml.png diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png Binary files differnew file mode 100644 index 00000000000..37dc7d71591 --- /dev/null +++ b/app/assets/images/emoji/flag_mm.png diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png Binary files differnew file mode 100644 index 00000000000..1f146bbcd1a --- /dev/null +++ b/app/assets/images/emoji/flag_mn.png diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png Binary files differnew file mode 100644 index 00000000000..7edde31f64b --- /dev/null +++ b/app/assets/images/emoji/flag_mo.png diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png Binary files differnew file mode 100644 index 00000000000..17ec1c441ed --- /dev/null +++ b/app/assets/images/emoji/flag_mp.png diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png Binary files differnew file mode 100644 index 00000000000..1e672dc9087 --- /dev/null +++ b/app/assets/images/emoji/flag_mq.png diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png Binary files differnew file mode 100644 index 00000000000..f87de46effe --- /dev/null +++ b/app/assets/images/emoji/flag_mr.png diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png Binary files differnew file mode 100644 index 00000000000..480b0d4ebda --- /dev/null +++ b/app/assets/images/emoji/flag_ms.png diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png Binary files differnew file mode 100644 index 00000000000..c9e1dbdce82 --- /dev/null +++ b/app/assets/images/emoji/flag_mt.png diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png Binary files differnew file mode 100644 index 00000000000..55b33cb7c33 --- /dev/null +++ b/app/assets/images/emoji/flag_mu.png diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png Binary files differnew file mode 100644 index 00000000000..ce5867126ae --- /dev/null +++ b/app/assets/images/emoji/flag_mv.png diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png Binary files differnew file mode 100644 index 00000000000..003d8548401 --- /dev/null +++ b/app/assets/images/emoji/flag_mw.png diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png Binary files differnew file mode 100644 index 00000000000..42572bcd0ba --- /dev/null +++ b/app/assets/images/emoji/flag_mx.png diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png Binary files differnew file mode 100644 index 00000000000..17526c26742 --- /dev/null +++ b/app/assets/images/emoji/flag_my.png diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png Binary files differnew file mode 100644 index 00000000000..2352a78e786 --- /dev/null +++ b/app/assets/images/emoji/flag_mz.png diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png Binary files differnew file mode 100644 index 00000000000..ed31c3df04d --- /dev/null +++ b/app/assets/images/emoji/flag_na.png diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png Binary files differnew file mode 100644 index 00000000000..90b3afebfa3 --- /dev/null +++ b/app/assets/images/emoji/flag_nc.png diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png Binary files differnew file mode 100644 index 00000000000..f98a1173c2a --- /dev/null +++ b/app/assets/images/emoji/flag_ne.png diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png Binary files differnew file mode 100644 index 00000000000..9099e767420 --- /dev/null +++ b/app/assets/images/emoji/flag_nf.png diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png Binary files differnew file mode 100644 index 00000000000..ea0abeff1a1 --- /dev/null +++ b/app/assets/images/emoji/flag_ng.png diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png Binary files differnew file mode 100644 index 00000000000..772920dfa10 --- /dev/null +++ b/app/assets/images/emoji/flag_ni.png diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png Binary files differnew file mode 100644 index 00000000000..83a0e817e41 --- /dev/null +++ b/app/assets/images/emoji/flag_nl.png diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png Binary files differnew file mode 100644 index 00000000000..99d3142eb7b --- /dev/null +++ b/app/assets/images/emoji/flag_no.png diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png Binary files differnew file mode 100644 index 00000000000..87425a8dfef --- /dev/null +++ b/app/assets/images/emoji/flag_np.png diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png Binary files differnew file mode 100644 index 00000000000..b3e3a5d5621 --- /dev/null +++ b/app/assets/images/emoji/flag_nr.png diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png Binary files differnew file mode 100644 index 00000000000..f03614443ee --- /dev/null +++ b/app/assets/images/emoji/flag_nu.png diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png Binary files differnew file mode 100644 index 00000000000..a4eeeab9cd9 --- /dev/null +++ b/app/assets/images/emoji/flag_nz.png diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png Binary files differnew file mode 100644 index 00000000000..ea824ba31e7 --- /dev/null +++ b/app/assets/images/emoji/flag_om.png diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png Binary files differnew file mode 100644 index 00000000000..c3091d89889 --- /dev/null +++ b/app/assets/images/emoji/flag_pa.png diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png Binary files differnew file mode 100644 index 00000000000..39223aa9dbb --- /dev/null +++ b/app/assets/images/emoji/flag_pe.png diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png Binary files differnew file mode 100644 index 00000000000..113445f8f6e --- /dev/null +++ b/app/assets/images/emoji/flag_pf.png diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png Binary files differnew file mode 100644 index 00000000000..825e9dcb762 --- /dev/null +++ b/app/assets/images/emoji/flag_pg.png diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png Binary files differnew file mode 100644 index 00000000000..8260e15bd2c --- /dev/null +++ b/app/assets/images/emoji/flag_ph.png diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png Binary files differnew file mode 100644 index 00000000000..a7b6a1c5074 --- /dev/null +++ b/app/assets/images/emoji/flag_pk.png diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png Binary files differnew file mode 100644 index 00000000000..19de2edec11 --- /dev/null +++ b/app/assets/images/emoji/flag_pl.png diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png Binary files differnew file mode 100644 index 00000000000..2ca60554193 --- /dev/null +++ b/app/assets/images/emoji/flag_pm.png diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png Binary files differnew file mode 100644 index 00000000000..f2263b154bc --- /dev/null +++ b/app/assets/images/emoji/flag_pn.png diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png Binary files differnew file mode 100644 index 00000000000..d0209cddb79 --- /dev/null +++ b/app/assets/images/emoji/flag_pr.png diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png Binary files differnew file mode 100644 index 00000000000..7ccab09778b --- /dev/null +++ b/app/assets/images/emoji/flag_ps.png diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png Binary files differnew file mode 100644 index 00000000000..cc93f27c64b --- /dev/null +++ b/app/assets/images/emoji/flag_pt.png diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png Binary files differnew file mode 100644 index 00000000000..154b2f12d3c --- /dev/null +++ b/app/assets/images/emoji/flag_pw.png diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png Binary files differnew file mode 100644 index 00000000000..662ad2f6ff1 --- /dev/null +++ b/app/assets/images/emoji/flag_py.png diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png Binary files differnew file mode 100644 index 00000000000..a01d8b05cc7 --- /dev/null +++ b/app/assets/images/emoji/flag_qa.png diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png Binary files differnew file mode 100644 index 00000000000..57f2bbe9df8 --- /dev/null +++ b/app/assets/images/emoji/flag_re.png diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png Binary files differnew file mode 100644 index 00000000000..3e48c447706 --- /dev/null +++ b/app/assets/images/emoji/flag_ro.png diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png Binary files differnew file mode 100644 index 00000000000..9df6c9a5235 --- /dev/null +++ b/app/assets/images/emoji/flag_rs.png diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png Binary files differnew file mode 100644 index 00000000000..e50c9db90e7 --- /dev/null +++ b/app/assets/images/emoji/flag_ru.png diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png Binary files differnew file mode 100644 index 00000000000..c238c874e1d --- /dev/null +++ b/app/assets/images/emoji/flag_rw.png diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png Binary files differnew file mode 100644 index 00000000000..4941be7d198 --- /dev/null +++ b/app/assets/images/emoji/flag_sa.png diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png Binary files differnew file mode 100644 index 00000000000..7d8f1ac6130 --- /dev/null +++ b/app/assets/images/emoji/flag_sb.png diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png Binary files differnew file mode 100644 index 00000000000..6ae4d90765e --- /dev/null +++ b/app/assets/images/emoji/flag_sc.png diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png Binary files differnew file mode 100644 index 00000000000..963be1b36fb --- /dev/null +++ b/app/assets/images/emoji/flag_sd.png diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png Binary files differnew file mode 100644 index 00000000000..fc0d0e0ce89 --- /dev/null +++ b/app/assets/images/emoji/flag_se.png diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png Binary files differnew file mode 100644 index 00000000000..de3c7737c42 --- /dev/null +++ b/app/assets/images/emoji/flag_sg.png diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png Binary files differnew file mode 100644 index 00000000000..40cd9e44e96 --- /dev/null +++ b/app/assets/images/emoji/flag_sh.png diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png Binary files differnew file mode 100644 index 00000000000..e308999dba2 --- /dev/null +++ b/app/assets/images/emoji/flag_si.png diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png Binary files differnew file mode 100644 index 00000000000..5884e648228 --- /dev/null +++ b/app/assets/images/emoji/flag_sj.png diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png Binary files differnew file mode 100644 index 00000000000..4259d0e1418 --- /dev/null +++ b/app/assets/images/emoji/flag_sk.png diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png Binary files differnew file mode 100644 index 00000000000..d2cc68830ab --- /dev/null +++ b/app/assets/images/emoji/flag_sl.png diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png Binary files differnew file mode 100644 index 00000000000..03b8708754e --- /dev/null +++ b/app/assets/images/emoji/flag_sm.png diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png Binary files differnew file mode 100644 index 00000000000..5368bbe93df --- /dev/null +++ b/app/assets/images/emoji/flag_sn.png diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png Binary files differnew file mode 100644 index 00000000000..68a0597365a --- /dev/null +++ b/app/assets/images/emoji/flag_so.png diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png Binary files differnew file mode 100644 index 00000000000..d3251327035 --- /dev/null +++ b/app/assets/images/emoji/flag_sr.png diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png Binary files differnew file mode 100644 index 00000000000..122977e798f --- /dev/null +++ b/app/assets/images/emoji/flag_ss.png diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png Binary files differnew file mode 100644 index 00000000000..f83a863d612 --- /dev/null +++ b/app/assets/images/emoji/flag_st.png diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png Binary files differnew file mode 100644 index 00000000000..efb83e2f253 --- /dev/null +++ b/app/assets/images/emoji/flag_sv.png diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png Binary files differnew file mode 100644 index 00000000000..94b760fbedf --- /dev/null +++ b/app/assets/images/emoji/flag_sx.png diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png Binary files differnew file mode 100644 index 00000000000..09a8ee8f78c --- /dev/null +++ b/app/assets/images/emoji/flag_sy.png diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png Binary files differnew file mode 100644 index 00000000000..f74e82ea1fd --- /dev/null +++ b/app/assets/images/emoji/flag_sz.png diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png Binary files differnew file mode 100644 index 00000000000..b44283e90e2 --- /dev/null +++ b/app/assets/images/emoji/flag_ta.png diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png Binary files differnew file mode 100644 index 00000000000..156b33d1ba6 --- /dev/null +++ b/app/assets/images/emoji/flag_tc.png diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png Binary files differnew file mode 100644 index 00000000000..ebe7f592828 --- /dev/null +++ b/app/assets/images/emoji/flag_td.png diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png Binary files differnew file mode 100644 index 00000000000..a1a3ad68ee2 --- /dev/null +++ b/app/assets/images/emoji/flag_tf.png diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png Binary files differnew file mode 100644 index 00000000000..826b73c9ac5 --- /dev/null +++ b/app/assets/images/emoji/flag_tg.png diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png Binary files differnew file mode 100644 index 00000000000..93ff542c5a6 --- /dev/null +++ b/app/assets/images/emoji/flag_th.png diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png Binary files differnew file mode 100644 index 00000000000..7a8a0b6190a --- /dev/null +++ b/app/assets/images/emoji/flag_tj.png diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png Binary files differnew file mode 100644 index 00000000000..2fa5a21b1bb --- /dev/null +++ b/app/assets/images/emoji/flag_tk.png diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png Binary files differnew file mode 100644 index 00000000000..5b120eccc6f --- /dev/null +++ b/app/assets/images/emoji/flag_tl.png diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png Binary files differnew file mode 100644 index 00000000000..c3c4f532302 --- /dev/null +++ b/app/assets/images/emoji/flag_tm.png diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png Binary files differnew file mode 100644 index 00000000000..58ef161229f --- /dev/null +++ b/app/assets/images/emoji/flag_tn.png diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png Binary files differnew file mode 100644 index 00000000000..1ffa7bb9d19 --- /dev/null +++ b/app/assets/images/emoji/flag_to.png diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png Binary files differnew file mode 100644 index 00000000000..325251fae88 --- /dev/null +++ b/app/assets/images/emoji/flag_tr.png diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png Binary files differnew file mode 100644 index 00000000000..ed3bb39a300 --- /dev/null +++ b/app/assets/images/emoji/flag_tt.png diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png Binary files differnew file mode 100644 index 00000000000..e82c65c7bb9 --- /dev/null +++ b/app/assets/images/emoji/flag_tv.png diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png Binary files differnew file mode 100644 index 00000000000..3a8f00b5928 --- /dev/null +++ b/app/assets/images/emoji/flag_tw.png diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png Binary files differnew file mode 100644 index 00000000000..2a020853d4e --- /dev/null +++ b/app/assets/images/emoji/flag_tz.png diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png Binary files differnew file mode 100644 index 00000000000..cd84d1bbd36 --- /dev/null +++ b/app/assets/images/emoji/flag_ua.png diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png Binary files differnew file mode 100644 index 00000000000..dc97690eb55 --- /dev/null +++ b/app/assets/images/emoji/flag_ug.png diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png Binary files differnew file mode 100644 index 00000000000..4a7ee3cdf13 --- /dev/null +++ b/app/assets/images/emoji/flag_um.png diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png Binary files differnew file mode 100644 index 00000000000..9f730305860 --- /dev/null +++ b/app/assets/images/emoji/flag_us.png diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png Binary files differnew file mode 100644 index 00000000000..b8002a697a6 --- /dev/null +++ b/app/assets/images/emoji/flag_uy.png diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png Binary files differnew file mode 100644 index 00000000000..d56ca9bc424 --- /dev/null +++ b/app/assets/images/emoji/flag_uz.png diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png Binary files differnew file mode 100644 index 00000000000..ddaf5e3141b --- /dev/null +++ b/app/assets/images/emoji/flag_va.png diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png Binary files differnew file mode 100644 index 00000000000..43703c62a71 --- /dev/null +++ b/app/assets/images/emoji/flag_vc.png diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png Binary files differnew file mode 100644 index 00000000000..1b62796824e --- /dev/null +++ b/app/assets/images/emoji/flag_ve.png diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png Binary files differnew file mode 100644 index 00000000000..536f780f1c0 --- /dev/null +++ b/app/assets/images/emoji/flag_vg.png diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png Binary files differnew file mode 100644 index 00000000000..64102012cfe --- /dev/null +++ b/app/assets/images/emoji/flag_vi.png diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png Binary files differnew file mode 100644 index 00000000000..427036046b6 --- /dev/null +++ b/app/assets/images/emoji/flag_vn.png diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png Binary files differnew file mode 100644 index 00000000000..706eba44070 --- /dev/null +++ b/app/assets/images/emoji/flag_vu.png diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_wf.png diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png Binary files differnew file mode 100644 index 00000000000..86d6e96d5e9 --- /dev/null +++ b/app/assets/images/emoji/flag_white.png diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png Binary files differnew file mode 100644 index 00000000000..a1ea0703141 --- /dev/null +++ b/app/assets/images/emoji/flag_ws.png diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png Binary files differnew file mode 100644 index 00000000000..e587a446632 --- /dev/null +++ b/app/assets/images/emoji/flag_xk.png diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png Binary files differnew file mode 100644 index 00000000000..eadfebd5f67 --- /dev/null +++ b/app/assets/images/emoji/flag_ye.png diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png Binary files differnew file mode 100644 index 00000000000..c81fa6d886e --- /dev/null +++ b/app/assets/images/emoji/flag_yt.png diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png Binary files differnew file mode 100644 index 00000000000..f397ef5072f --- /dev/null +++ b/app/assets/images/emoji/flag_za.png diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png Binary files differnew file mode 100644 index 00000000000..2494a31f662 --- /dev/null +++ b/app/assets/images/emoji/flag_zm.png diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png Binary files differnew file mode 100644 index 00000000000..e09b9652be6 --- /dev/null +++ b/app/assets/images/emoji/flag_zw.png diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png Binary files differnew file mode 100644 index 00000000000..3b451035a3a --- /dev/null +++ b/app/assets/images/emoji/flags.png diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png Binary files differnew file mode 100644 index 00000000000..eee36c25067 --- /dev/null +++ b/app/assets/images/emoji/flashlight.png diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png Binary files differnew file mode 100644 index 00000000000..c9250d27fa7 --- /dev/null +++ b/app/assets/images/emoji/fleur-de-lis.png diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png Binary files differnew file mode 100644 index 00000000000..072a76d3c13 --- /dev/null +++ b/app/assets/images/emoji/floppy_disk.png diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png Binary files differnew file mode 100644 index 00000000000..6766b044d95 --- /dev/null +++ b/app/assets/images/emoji/flower_playing_cards.png diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png Binary files differnew file mode 100644 index 00000000000..829220bc470 --- /dev/null +++ b/app/assets/images/emoji/flushed.png diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png Binary files differnew file mode 100644 index 00000000000..4e73c2de272 --- /dev/null +++ b/app/assets/images/emoji/fog.png diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png Binary files differnew file mode 100644 index 00000000000..57702d8d3ac --- /dev/null +++ b/app/assets/images/emoji/foggy.png diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png Binary files differnew file mode 100644 index 00000000000..10366f41fce --- /dev/null +++ b/app/assets/images/emoji/football.png diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png Binary files differnew file mode 100644 index 00000000000..b2673c5a1a8 --- /dev/null +++ b/app/assets/images/emoji/footprints.png diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png Binary files differnew file mode 100644 index 00000000000..09f1feaea1c --- /dev/null +++ b/app/assets/images/emoji/fork_and_knife.png diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png Binary files differnew file mode 100644 index 00000000000..7411755f708 --- /dev/null +++ b/app/assets/images/emoji/fork_knife_plate.png diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png Binary files differnew file mode 100644 index 00000000000..293f5d91c0f --- /dev/null +++ b/app/assets/images/emoji/fountain.png diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png Binary files differnew file mode 100644 index 00000000000..b0e914aac45 --- /dev/null +++ b/app/assets/images/emoji/four.png diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png Binary files differnew file mode 100644 index 00000000000..fdedfcc2b4e --- /dev/null +++ b/app/assets/images/emoji/four_leaf_clover.png diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png Binary files differnew file mode 100644 index 00000000000..1ab339bf054 --- /dev/null +++ b/app/assets/images/emoji/fox.png diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png Binary files differnew file mode 100644 index 00000000000..9fe84607bfd --- /dev/null +++ b/app/assets/images/emoji/frame_photo.png diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png Binary files differnew file mode 100644 index 00000000000..b71956eb48a --- /dev/null +++ b/app/assets/images/emoji/free.png diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png Binary files differnew file mode 100644 index 00000000000..4c2c5639822 --- /dev/null +++ b/app/assets/images/emoji/french_bread.png diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png Binary files differnew file mode 100644 index 00000000000..752ba7f1398 --- /dev/null +++ b/app/assets/images/emoji/fried_shrimp.png diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png Binary files differnew file mode 100644 index 00000000000..4e2a4caacef --- /dev/null +++ b/app/assets/images/emoji/fries.png diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png Binary files differnew file mode 100644 index 00000000000..8825d1ad577 --- /dev/null +++ b/app/assets/images/emoji/frog.png diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png Binary files differnew file mode 100644 index 00000000000..43ab6b0a1c1 --- /dev/null +++ b/app/assets/images/emoji/frowning.png diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png Binary files differnew file mode 100644 index 00000000000..6ae71f233b9 --- /dev/null +++ b/app/assets/images/emoji/frowning2.png diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png Binary files differnew file mode 100644 index 00000000000..05b18794474 --- /dev/null +++ b/app/assets/images/emoji/fuelpump.png diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png Binary files differnew file mode 100644 index 00000000000..c9a2d6aa7c9 --- /dev/null +++ b/app/assets/images/emoji/full_moon.png diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..a5c25bbaf64 --- /dev/null +++ b/app/assets/images/emoji/full_moon_with_face.png diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png Binary files differnew file mode 100644 index 00000000000..ad3626fe5e5 --- /dev/null +++ b/app/assets/images/emoji/game_die.png diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png Binary files differnew file mode 100644 index 00000000000..2a1cc2c0ff4 --- /dev/null +++ b/app/assets/images/emoji/gear.png diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png Binary files differnew file mode 100644 index 00000000000..db122d26a19 --- /dev/null +++ b/app/assets/images/emoji/gem.png diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png Binary files differnew file mode 100644 index 00000000000..1a09698cf00 --- /dev/null +++ b/app/assets/images/emoji/gemini.png diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png Binary files differnew file mode 100644 index 00000000000..5650bc0ed18 --- /dev/null +++ b/app/assets/images/emoji/ghost.png diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png Binary files differnew file mode 100644 index 00000000000..844e2164560 --- /dev/null +++ b/app/assets/images/emoji/gift.png diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png Binary files differnew file mode 100644 index 00000000000..902ceafe4d1 --- /dev/null +++ b/app/assets/images/emoji/gift_heart.png diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png Binary files differnew file mode 100644 index 00000000000..dc1d4d08b39 --- /dev/null +++ b/app/assets/images/emoji/girl.png diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png Binary files differnew file mode 100644 index 00000000000..bb667e88651 --- /dev/null +++ b/app/assets/images/emoji/girl_tone1.png diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png Binary files differnew file mode 100644 index 00000000000..a59ed4a3f0d --- /dev/null +++ b/app/assets/images/emoji/girl_tone2.png diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png Binary files differnew file mode 100644 index 00000000000..517e7f2a7b0 --- /dev/null +++ b/app/assets/images/emoji/girl_tone3.png diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png Binary files differnew file mode 100644 index 00000000000..542d96c8487 --- /dev/null +++ b/app/assets/images/emoji/girl_tone4.png diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png Binary files differnew file mode 100644 index 00000000000..66b7c28c2df --- /dev/null +++ b/app/assets/images/emoji/girl_tone5.png diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png Binary files differnew file mode 100644 index 00000000000..82450c1a4ba --- /dev/null +++ b/app/assets/images/emoji/globe_with_meridians.png diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png Binary files differnew file mode 100644 index 00000000000..df3a53da0fb --- /dev/null +++ b/app/assets/images/emoji/goal.png diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png Binary files differnew file mode 100644 index 00000000000..f9d9e38a128 --- /dev/null +++ b/app/assets/images/emoji/goat.png diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png Binary files differnew file mode 100644 index 00000000000..f65a21d8a46 --- /dev/null +++ b/app/assets/images/emoji/golf.png diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png Binary files differnew file mode 100644 index 00000000000..39c552de86d --- /dev/null +++ b/app/assets/images/emoji/golfer.png diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png Binary files differnew file mode 100644 index 00000000000..acc51e13622 --- /dev/null +++ b/app/assets/images/emoji/gorilla.png diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png Binary files differnew file mode 100644 index 00000000000..30d22218896 --- /dev/null +++ b/app/assets/images/emoji/grapes.png diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png Binary files differnew file mode 100644 index 00000000000..5fd51bd3915 --- /dev/null +++ b/app/assets/images/emoji/green_apple.png diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png Binary files differnew file mode 100644 index 00000000000..e5e411cf3b5 --- /dev/null +++ b/app/assets/images/emoji/green_book.png diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png Binary files differnew file mode 100644 index 00000000000..c52d60a58be --- /dev/null +++ b/app/assets/images/emoji/green_heart.png diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png Binary files differnew file mode 100644 index 00000000000..9b64da8bf7f --- /dev/null +++ b/app/assets/images/emoji/grey_exclamation.png diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png Binary files differnew file mode 100644 index 00000000000..6e7824c75f6 --- /dev/null +++ b/app/assets/images/emoji/grey_question.png diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png Binary files differnew file mode 100644 index 00000000000..871b2f071c9 --- /dev/null +++ b/app/assets/images/emoji/grimacing.png diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png Binary files differnew file mode 100644 index 00000000000..418d94c811b --- /dev/null +++ b/app/assets/images/emoji/grin.png diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png Binary files differnew file mode 100644 index 00000000000..3e8e0dab78c --- /dev/null +++ b/app/assets/images/emoji/grinning.png diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png Binary files differnew file mode 100644 index 00000000000..8d7ab3c473c --- /dev/null +++ b/app/assets/images/emoji/guardsman.png diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png Binary files differnew file mode 100644 index 00000000000..cea9ba27468 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone1.png diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png Binary files differnew file mode 100644 index 00000000000..037464e4028 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone2.png diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png Binary files differnew file mode 100644 index 00000000000..0f6726fbe87 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone3.png diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png Binary files differnew file mode 100644 index 00000000000..85fcf9a3b97 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone4.png diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png Binary files differnew file mode 100644 index 00000000000..e5f9ca7d5a2 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone5.png diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png Binary files differnew file mode 100644 index 00000000000..43d752f1e3d --- /dev/null +++ b/app/assets/images/emoji/guitar.png diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png Binary files differnew file mode 100644 index 00000000000..89c5c244c7b --- /dev/null +++ b/app/assets/images/emoji/gun.png diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png Binary files differnew file mode 100644 index 00000000000..91266b12930 --- /dev/null +++ b/app/assets/images/emoji/haircut.png diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png Binary files differnew file mode 100644 index 00000000000..c743b74abeb --- /dev/null +++ b/app/assets/images/emoji/haircut_tone1.png diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png Binary files differnew file mode 100644 index 00000000000..f144f8e55ce --- /dev/null +++ b/app/assets/images/emoji/haircut_tone2.png diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png Binary files differnew file mode 100644 index 00000000000..d5ad19563ac --- /dev/null +++ b/app/assets/images/emoji/haircut_tone3.png diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png Binary files differnew file mode 100644 index 00000000000..244fd3af008 --- /dev/null +++ b/app/assets/images/emoji/haircut_tone4.png diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png Binary files differnew file mode 100644 index 00000000000..20a94a88623 --- /dev/null +++ b/app/assets/images/emoji/haircut_tone5.png diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png Binary files differnew file mode 100644 index 00000000000..3573b28a1fd --- /dev/null +++ b/app/assets/images/emoji/hamburger.png diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png Binary files differnew file mode 100644 index 00000000000..00736cce47d --- /dev/null +++ b/app/assets/images/emoji/hammer.png diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png Binary files differnew file mode 100644 index 00000000000..3bee30ec588 --- /dev/null +++ b/app/assets/images/emoji/hammer_pick.png diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png Binary files differnew file mode 100644 index 00000000000..9a04388e4e7 --- /dev/null +++ b/app/assets/images/emoji/hamster.png diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png Binary files differnew file mode 100644 index 00000000000..fb5ae8ebb5a --- /dev/null +++ b/app/assets/images/emoji/hand_splayed.png diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png Binary files differnew file mode 100644 index 00000000000..a7888e6bd23 --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone1.png diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png Binary files differnew file mode 100644 index 00000000000..cc10fbc272d --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone2.png diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png Binary files differnew file mode 100644 index 00000000000..707236ae8a4 --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone3.png diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png Binary files differnew file mode 100644 index 00000000000..1430df9c61f --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone4.png diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png Binary files differnew file mode 100644 index 00000000000..80bec971b6b --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone5.png diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png Binary files differnew file mode 100644 index 00000000000..cbf75c5d25e --- /dev/null +++ b/app/assets/images/emoji/handbag.png diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png Binary files differnew file mode 100644 index 00000000000..1152f1344c7 --- /dev/null +++ b/app/assets/images/emoji/handball.png diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png Binary files differnew file mode 100644 index 00000000000..c26cac2df98 --- /dev/null +++ b/app/assets/images/emoji/handball_tone1.png diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png Binary files differnew file mode 100644 index 00000000000..7baaf95a9a2 --- /dev/null +++ b/app/assets/images/emoji/handball_tone2.png diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png Binary files differnew file mode 100644 index 00000000000..0e3a37c3d40 --- /dev/null +++ b/app/assets/images/emoji/handball_tone3.png diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png Binary files differnew file mode 100644 index 00000000000..e1233f38266 --- /dev/null +++ b/app/assets/images/emoji/handball_tone4.png diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png Binary files differnew file mode 100644 index 00000000000..6b1eb9b64b0 --- /dev/null +++ b/app/assets/images/emoji/handball_tone5.png diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png Binary files differnew file mode 100644 index 00000000000..c5d35fd8138 --- /dev/null +++ b/app/assets/images/emoji/handshake.png diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png Binary files differnew file mode 100644 index 00000000000..8f8fbb9bdca --- /dev/null +++ b/app/assets/images/emoji/handshake_tone1.png diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png Binary files differnew file mode 100644 index 00000000000..336a77a6d78 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone2.png diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png Binary files differnew file mode 100644 index 00000000000..95f62d4fecd --- /dev/null +++ b/app/assets/images/emoji/handshake_tone3.png diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png Binary files differnew file mode 100644 index 00000000000..2b0a6433886 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone4.png diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png Binary files differnew file mode 100644 index 00000000000..40189ee68e4 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone5.png diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png Binary files differnew file mode 100644 index 00000000000..6e26f0070b0 --- /dev/null +++ b/app/assets/images/emoji/hash.png diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png Binary files differnew file mode 100644 index 00000000000..31dfb511e0e --- /dev/null +++ b/app/assets/images/emoji/hatched_chick.png diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png Binary files differnew file mode 100644 index 00000000000..c5b0e8f3bcc --- /dev/null +++ b/app/assets/images/emoji/hatching_chick.png diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png Binary files differnew file mode 100644 index 00000000000..0be723085e0 --- /dev/null +++ b/app/assets/images/emoji/head_bandage.png diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png Binary files differnew file mode 100644 index 00000000000..e9fd34041d8 --- /dev/null +++ b/app/assets/images/emoji/headphones.png diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png Binary files differnew file mode 100644 index 00000000000..74b6be0c6c5 --- /dev/null +++ b/app/assets/images/emoji/hear_no_evil.png diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png Binary files differnew file mode 100644 index 00000000000..638cb72dc4e --- /dev/null +++ b/app/assets/images/emoji/heart.png diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png Binary files differnew file mode 100644 index 00000000000..5443f60bc63 --- /dev/null +++ b/app/assets/images/emoji/heart_decoration.png diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png Binary files differnew file mode 100644 index 00000000000..91b520be40b --- /dev/null +++ b/app/assets/images/emoji/heart_exclamation.png diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png Binary files differnew file mode 100644 index 00000000000..73fbee29d4e --- /dev/null +++ b/app/assets/images/emoji/heart_eyes.png diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png Binary files differnew file mode 100644 index 00000000000..bc5a833f9a1 --- /dev/null +++ b/app/assets/images/emoji/heart_eyes_cat.png diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png Binary files differnew file mode 100644 index 00000000000..0bcf2d1d567 --- /dev/null +++ b/app/assets/images/emoji/heartbeat.png diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png Binary files differnew file mode 100644 index 00000000000..d6e694e972f --- /dev/null +++ b/app/assets/images/emoji/heartpulse.png diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png Binary files differnew file mode 100644 index 00000000000..393c3ed5267 --- /dev/null +++ b/app/assets/images/emoji/hearts.png diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png Binary files differnew file mode 100644 index 00000000000..03bd695377e --- /dev/null +++ b/app/assets/images/emoji/heavy_check_mark.png diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png Binary files differnew file mode 100644 index 00000000000..df32ab21bea --- /dev/null +++ b/app/assets/images/emoji/heavy_division_sign.png diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png Binary files differnew file mode 100644 index 00000000000..ef2c2e20590 --- /dev/null +++ b/app/assets/images/emoji/heavy_dollar_sign.png diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png Binary files differnew file mode 100644 index 00000000000..054211caf12 --- /dev/null +++ b/app/assets/images/emoji/heavy_minus_sign.png diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png Binary files differnew file mode 100644 index 00000000000..e47cc1b685d --- /dev/null +++ b/app/assets/images/emoji/heavy_multiplication_x.png diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png Binary files differnew file mode 100644 index 00000000000..40799798aaf --- /dev/null +++ b/app/assets/images/emoji/heavy_plus_sign.png diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png Binary files differnew file mode 100644 index 00000000000..7ec5f39a51a --- /dev/null +++ b/app/assets/images/emoji/helicopter.png diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png Binary files differnew file mode 100644 index 00000000000..7140a676038 --- /dev/null +++ b/app/assets/images/emoji/helmet_with_cross.png diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png Binary files differnew file mode 100644 index 00000000000..d984d1562bb --- /dev/null +++ b/app/assets/images/emoji/herb.png diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png Binary files differnew file mode 100644 index 00000000000..39dd3524233 --- /dev/null +++ b/app/assets/images/emoji/hibiscus.png diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png Binary files differnew file mode 100644 index 00000000000..c41f2d5fd50 --- /dev/null +++ b/app/assets/images/emoji/high_brightness.png diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png Binary files differnew file mode 100644 index 00000000000..b331cbccc9d --- /dev/null +++ b/app/assets/images/emoji/high_heel.png diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png Binary files differnew file mode 100644 index 00000000000..be94e9cbf73 --- /dev/null +++ b/app/assets/images/emoji/hockey.png diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png Binary files differnew file mode 100644 index 00000000000..517d2ae0deb --- /dev/null +++ b/app/assets/images/emoji/hole.png diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png Binary files differnew file mode 100644 index 00000000000..6ab4a2a2651 --- /dev/null +++ b/app/assets/images/emoji/homes.png diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png Binary files differnew file mode 100644 index 00000000000..9d8f592955e --- /dev/null +++ b/app/assets/images/emoji/honey_pot.png diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png Binary files differnew file mode 100644 index 00000000000..7cb1172f4e4 --- /dev/null +++ b/app/assets/images/emoji/horse.png diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png Binary files differnew file mode 100644 index 00000000000..addf9edac56 --- /dev/null +++ b/app/assets/images/emoji/horse_racing.png diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png Binary files differnew file mode 100644 index 00000000000..e9bf4092e98 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone1.png diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png Binary files differnew file mode 100644 index 00000000000..031bbc3d867 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone2.png diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png Binary files differnew file mode 100644 index 00000000000..b40ef891f9b --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone3.png diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png Binary files differnew file mode 100644 index 00000000000..e286cb85065 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone4.png diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png Binary files differnew file mode 100644 index 00000000000..453c51c6007 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone5.png diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png Binary files differnew file mode 100644 index 00000000000..1cbce4ae767 --- /dev/null +++ b/app/assets/images/emoji/hospital.png diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png Binary files differnew file mode 100644 index 00000000000..266675bd577 --- /dev/null +++ b/app/assets/images/emoji/hot_pepper.png diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png Binary files differnew file mode 100644 index 00000000000..3c3354d94cb --- /dev/null +++ b/app/assets/images/emoji/hotdog.png diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png Binary files differnew file mode 100644 index 00000000000..ea8f4c4979a --- /dev/null +++ b/app/assets/images/emoji/hotel.png diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png Binary files differnew file mode 100644 index 00000000000..3d9df2d9475 --- /dev/null +++ b/app/assets/images/emoji/hotsprings.png diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png Binary files differnew file mode 100644 index 00000000000..a5db2d1d3f4 --- /dev/null +++ b/app/assets/images/emoji/hourglass.png diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png Binary files differnew file mode 100644 index 00000000000..b93b15ed6d8 --- /dev/null +++ b/app/assets/images/emoji/hourglass_flowing_sand.png diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png Binary files differnew file mode 100644 index 00000000000..01c98a0ba92 --- /dev/null +++ b/app/assets/images/emoji/house.png diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png Binary files differnew file mode 100644 index 00000000000..c55e81de990 --- /dev/null +++ b/app/assets/images/emoji/house_abandoned.png diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png Binary files differnew file mode 100644 index 00000000000..0aae41598ef --- /dev/null +++ b/app/assets/images/emoji/house_with_garden.png diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png Binary files differnew file mode 100644 index 00000000000..5bba6dc6d51 --- /dev/null +++ b/app/assets/images/emoji/hugging.png diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png Binary files differnew file mode 100644 index 00000000000..cad0e23132e --- /dev/null +++ b/app/assets/images/emoji/hushed.png diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png Binary files differnew file mode 100644 index 00000000000..94267b9c434 --- /dev/null +++ b/app/assets/images/emoji/ice_cream.png diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png Binary files differnew file mode 100644 index 00000000000..8c449b0c039 --- /dev/null +++ b/app/assets/images/emoji/ice_skate.png diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png Binary files differnew file mode 100644 index 00000000000..8f6546e31a5 --- /dev/null +++ b/app/assets/images/emoji/icecream.png diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png Binary files differnew file mode 100644 index 00000000000..5bf69bf7ba8 --- /dev/null +++ b/app/assets/images/emoji/id.png diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png Binary files differnew file mode 100644 index 00000000000..0c0d589caf0 --- /dev/null +++ b/app/assets/images/emoji/ideograph_advantage.png diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png Binary files differnew file mode 100644 index 00000000000..9f9a9605539 --- /dev/null +++ b/app/assets/images/emoji/imp.png diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png Binary files differnew file mode 100644 index 00000000000..41a6be2b0ee --- /dev/null +++ b/app/assets/images/emoji/inbox_tray.png diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png Binary files differnew file mode 100644 index 00000000000..fd22e88182e --- /dev/null +++ b/app/assets/images/emoji/incoming_envelope.png diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png Binary files differnew file mode 100644 index 00000000000..55fc6294d25 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person.png diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png Binary files differnew file mode 100644 index 00000000000..3d9e2247940 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone1.png diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png Binary files differnew file mode 100644 index 00000000000..879e8b7966d --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone2.png diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png Binary files differnew file mode 100644 index 00000000000..307514eab67 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone3.png diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png Binary files differnew file mode 100644 index 00000000000..297395dcb3f --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone4.png diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png Binary files differnew file mode 100644 index 00000000000..26f8f22b28b --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone5.png diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png Binary files differnew file mode 100644 index 00000000000..871f2db9314 --- /dev/null +++ b/app/assets/images/emoji/information_source.png diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png Binary files differnew file mode 100644 index 00000000000..57f5151124f --- /dev/null +++ b/app/assets/images/emoji/innocent.png diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png Binary files differnew file mode 100644 index 00000000000..509813e9bb2 --- /dev/null +++ b/app/assets/images/emoji/interrobang.png diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png Binary files differnew file mode 100644 index 00000000000..fd377acf872 --- /dev/null +++ b/app/assets/images/emoji/iphone.png diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png Binary files differnew file mode 100644 index 00000000000..7fd834389b7 --- /dev/null +++ b/app/assets/images/emoji/island.png diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png Binary files differnew file mode 100644 index 00000000000..dfd933f6f36 --- /dev/null +++ b/app/assets/images/emoji/izakaya_lantern.png diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png Binary files differnew file mode 100644 index 00000000000..44c3fc0aec9 --- /dev/null +++ b/app/assets/images/emoji/jack_o_lantern.png diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png Binary files differnew file mode 100644 index 00000000000..d86d0a59e12 --- /dev/null +++ b/app/assets/images/emoji/japan.png diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png Binary files differnew file mode 100644 index 00000000000..64b4e33a1ae --- /dev/null +++ b/app/assets/images/emoji/japanese_castle.png diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png Binary files differnew file mode 100644 index 00000000000..515c6a2250e --- /dev/null +++ b/app/assets/images/emoji/japanese_goblin.png diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png Binary files differnew file mode 100644 index 00000000000..fe8670fdaf1 --- /dev/null +++ b/app/assets/images/emoji/japanese_ogre.png diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png Binary files differnew file mode 100644 index 00000000000..2a6869d674c --- /dev/null +++ b/app/assets/images/emoji/jeans.png diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png Binary files differnew file mode 100644 index 00000000000..0ba3b1859d8 --- /dev/null +++ b/app/assets/images/emoji/joy.png diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png Binary files differnew file mode 100644 index 00000000000..aac353179aa --- /dev/null +++ b/app/assets/images/emoji/joy_cat.png diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png Binary files differnew file mode 100644 index 00000000000..1ee1905434e --- /dev/null +++ b/app/assets/images/emoji/joystick.png diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png Binary files differnew file mode 100644 index 00000000000..a37f6224a42 --- /dev/null +++ b/app/assets/images/emoji/juggling.png diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png Binary files differnew file mode 100644 index 00000000000..c18eda40031 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone1.png diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png Binary files differnew file mode 100644 index 00000000000..de3b7a555b6 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone2.png diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png Binary files differnew file mode 100644 index 00000000000..74ab6d85458 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone3.png diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png Binary files differnew file mode 100644 index 00000000000..1c57823203f --- /dev/null +++ b/app/assets/images/emoji/juggling_tone4.png diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png Binary files differnew file mode 100644 index 00000000000..c343d6ee98a --- /dev/null +++ b/app/assets/images/emoji/juggling_tone5.png diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png Binary files differnew file mode 100644 index 00000000000..1778c1138e4 --- /dev/null +++ b/app/assets/images/emoji/kaaba.png diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png Binary files differnew file mode 100644 index 00000000000..319cd1b884c --- /dev/null +++ b/app/assets/images/emoji/key.png diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png Binary files differnew file mode 100644 index 00000000000..e11d706c6c8 --- /dev/null +++ b/app/assets/images/emoji/key2.png diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png Binary files differnew file mode 100644 index 00000000000..75027cb9af7 --- /dev/null +++ b/app/assets/images/emoji/keyboard.png diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png Binary files differnew file mode 100644 index 00000000000..abe851115d1 --- /dev/null +++ b/app/assets/images/emoji/kimono.png diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png Binary files differnew file mode 100644 index 00000000000..85e6dcfc4e8 --- /dev/null +++ b/app/assets/images/emoji/kiss.png diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png Binary files differnew file mode 100644 index 00000000000..a9a0edae17c --- /dev/null +++ b/app/assets/images/emoji/kiss_mm.png diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png Binary files differnew file mode 100644 index 00000000000..fdac73cbb1d --- /dev/null +++ b/app/assets/images/emoji/kiss_ww.png diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png Binary files differnew file mode 100644 index 00000000000..39d325fd8e3 --- /dev/null +++ b/app/assets/images/emoji/kissing.png diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png Binary files differnew file mode 100644 index 00000000000..6e0bcc77540 --- /dev/null +++ b/app/assets/images/emoji/kissing_cat.png diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png Binary files differnew file mode 100644 index 00000000000..b684d7d4d6c --- /dev/null +++ b/app/assets/images/emoji/kissing_closed_eyes.png diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png Binary files differnew file mode 100644 index 00000000000..0ff808fd614 --- /dev/null +++ b/app/assets/images/emoji/kissing_heart.png diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png Binary files differnew file mode 100644 index 00000000000..e181f17099d --- /dev/null +++ b/app/assets/images/emoji/kissing_smiling_eyes.png diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png Binary files differnew file mode 100644 index 00000000000..dfbd8258074 --- /dev/null +++ b/app/assets/images/emoji/kiwi.png diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png Binary files differnew file mode 100644 index 00000000000..1acb9f3077b --- /dev/null +++ b/app/assets/images/emoji/knife.png diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png Binary files differnew file mode 100644 index 00000000000..a0aa437a98c --- /dev/null +++ b/app/assets/images/emoji/koala.png diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png Binary files differnew file mode 100644 index 00000000000..6450eb44d90 --- /dev/null +++ b/app/assets/images/emoji/koko.png diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png Binary files differnew file mode 100644 index 00000000000..d41c9b4f1e1 --- /dev/null +++ b/app/assets/images/emoji/label.png diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png Binary files differnew file mode 100644 index 00000000000..84078ef3127 --- /dev/null +++ b/app/assets/images/emoji/large_blue_circle.png diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png Binary files differnew file mode 100644 index 00000000000..416a58bd5a8 --- /dev/null +++ b/app/assets/images/emoji/large_blue_diamond.png diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png Binary files differnew file mode 100644 index 00000000000..73ff0ac36c8 --- /dev/null +++ b/app/assets/images/emoji/large_orange_diamond.png diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png Binary files differnew file mode 100644 index 00000000000..0842a0dd408 --- /dev/null +++ b/app/assets/images/emoji/last_quarter_moon.png diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..94099343c5d --- /dev/null +++ b/app/assets/images/emoji/last_quarter_moon_with_face.png diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png Binary files differnew file mode 100644 index 00000000000..d94e9505ba1 --- /dev/null +++ b/app/assets/images/emoji/laughing.png diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png Binary files differnew file mode 100644 index 00000000000..1e43e1af820 --- /dev/null +++ b/app/assets/images/emoji/leaves.png diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png Binary files differnew file mode 100644 index 00000000000..13e7561a4bd --- /dev/null +++ b/app/assets/images/emoji/ledger.png diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png Binary files differnew file mode 100644 index 00000000000..a9d9fd8d59c --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist.png diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png Binary files differnew file mode 100644 index 00000000000..1262a6b4b69 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone1.png diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png Binary files differnew file mode 100644 index 00000000000..40bf70b82b2 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone2.png diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png Binary files differnew file mode 100644 index 00000000000..93f58145111 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone3.png diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png Binary files differnew file mode 100644 index 00000000000..d82b5ec91f0 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone4.png diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png Binary files differnew file mode 100644 index 00000000000..09ae4cd492b --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone5.png diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png Binary files differnew file mode 100644 index 00000000000..887b23f3f25 --- /dev/null +++ b/app/assets/images/emoji/left_luggage.png diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png Binary files differnew file mode 100644 index 00000000000..7937f24f2ac --- /dev/null +++ b/app/assets/images/emoji/left_right_arrow.png diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png Binary files differnew file mode 100644 index 00000000000..ba45c2ad9e9 --- /dev/null +++ b/app/assets/images/emoji/leftwards_arrow_with_hook.png diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png Binary files differnew file mode 100644 index 00000000000..9a7d95ca220 --- /dev/null +++ b/app/assets/images/emoji/lemon.png diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png Binary files differnew file mode 100644 index 00000000000..30158d34de9 --- /dev/null +++ b/app/assets/images/emoji/leo.png diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png Binary files differnew file mode 100644 index 00000000000..8aac3d49448 --- /dev/null +++ b/app/assets/images/emoji/leopard.png diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png Binary files differnew file mode 100644 index 00000000000..720a3b34119 --- /dev/null +++ b/app/assets/images/emoji/level_slider.png diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png Binary files differnew file mode 100644 index 00000000000..3dc315a3d91 --- /dev/null +++ b/app/assets/images/emoji/levitate.png diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png Binary files differnew file mode 100644 index 00000000000..8fd133a357c --- /dev/null +++ b/app/assets/images/emoji/libra.png diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png Binary files differnew file mode 100644 index 00000000000..afdeaa476af --- /dev/null +++ b/app/assets/images/emoji/lifter.png diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png Binary files differnew file mode 100644 index 00000000000..febaad123ec --- /dev/null +++ b/app/assets/images/emoji/lifter_tone1.png diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png Binary files differnew file mode 100644 index 00000000000..27ae794a18e --- /dev/null +++ b/app/assets/images/emoji/lifter_tone2.png diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png Binary files differnew file mode 100644 index 00000000000..45c4c22c709 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone3.png diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png Binary files differnew file mode 100644 index 00000000000..67dd21d2464 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone4.png diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png Binary files differnew file mode 100644 index 00000000000..fa0152038b6 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone5.png diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png Binary files differnew file mode 100644 index 00000000000..a64829f5078 --- /dev/null +++ b/app/assets/images/emoji/light_rail.png diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png Binary files differnew file mode 100644 index 00000000000..ae20f0f8eec --- /dev/null +++ b/app/assets/images/emoji/link.png diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png Binary files differnew file mode 100644 index 00000000000..5062ab47ecf --- /dev/null +++ b/app/assets/images/emoji/lion_face.png diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png Binary files differnew file mode 100644 index 00000000000..35f3cc2006f --- /dev/null +++ b/app/assets/images/emoji/lips.png diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png Binary files differnew file mode 100644 index 00000000000..61a0c084c99 --- /dev/null +++ b/app/assets/images/emoji/lipstick.png diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png Binary files differnew file mode 100644 index 00000000000..8363876050e --- /dev/null +++ b/app/assets/images/emoji/lizard.png diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png Binary files differnew file mode 100644 index 00000000000..5a739c46644 --- /dev/null +++ b/app/assets/images/emoji/lock.png diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png Binary files differnew file mode 100644 index 00000000000..19a07d162fb --- /dev/null +++ b/app/assets/images/emoji/lock_with_ink_pen.png diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png Binary files differnew file mode 100644 index 00000000000..ad76d7bf916 --- /dev/null +++ b/app/assets/images/emoji/lollipop.png diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png Binary files differnew file mode 100644 index 00000000000..0b82c8fe315 --- /dev/null +++ b/app/assets/images/emoji/loop.png diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png Binary files differnew file mode 100644 index 00000000000..8370033a539 --- /dev/null +++ b/app/assets/images/emoji/loud_sound.png diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png Binary files differnew file mode 100644 index 00000000000..5fd76a95b82 --- /dev/null +++ b/app/assets/images/emoji/loudspeaker.png diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png Binary files differnew file mode 100644 index 00000000000..5e136be6f8b --- /dev/null +++ b/app/assets/images/emoji/love_hotel.png diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png Binary files differnew file mode 100644 index 00000000000..3c3c767e784 --- /dev/null +++ b/app/assets/images/emoji/love_letter.png diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png Binary files differnew file mode 100644 index 00000000000..543011d3961 --- /dev/null +++ b/app/assets/images/emoji/low_brightness.png diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png Binary files differnew file mode 100644 index 00000000000..02827e2628b --- /dev/null +++ b/app/assets/images/emoji/lying_face.png diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png Binary files differnew file mode 100644 index 00000000000..8a3506fc1d7 --- /dev/null +++ b/app/assets/images/emoji/m.png diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png Binary files differnew file mode 100644 index 00000000000..55487156ac6 --- /dev/null +++ b/app/assets/images/emoji/mag.png diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png Binary files differnew file mode 100644 index 00000000000..0f4b1bca876 --- /dev/null +++ b/app/assets/images/emoji/mag_right.png diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png Binary files differnew file mode 100644 index 00000000000..66fd32025b2 --- /dev/null +++ b/app/assets/images/emoji/mahjong.png diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png Binary files differnew file mode 100644 index 00000000000..ef5174e40dd --- /dev/null +++ b/app/assets/images/emoji/mailbox.png diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png Binary files differnew file mode 100644 index 00000000000..ddc705db0d8 --- /dev/null +++ b/app/assets/images/emoji/mailbox_closed.png diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png Binary files differnew file mode 100644 index 00000000000..5460616a5b1 --- /dev/null +++ b/app/assets/images/emoji/mailbox_with_mail.png diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png Binary files differnew file mode 100644 index 00000000000..f9aeee6b15a --- /dev/null +++ b/app/assets/images/emoji/mailbox_with_no_mail.png diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png Binary files differnew file mode 100644 index 00000000000..857a02e5146 --- /dev/null +++ b/app/assets/images/emoji/man.png diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png Binary files differnew file mode 100644 index 00000000000..ccff3bede5a --- /dev/null +++ b/app/assets/images/emoji/man_dancing.png diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png Binary files differnew file mode 100644 index 00000000000..e0b9f82d905 --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone1.png diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png Binary files differnew file mode 100644 index 00000000000..a5beed56e2e --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone2.png diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png Binary files differnew file mode 100644 index 00000000000..2fa20180a6e --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone3.png diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png Binary files differnew file mode 100644 index 00000000000..bd3528c83ba --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone4.png diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png Binary files differnew file mode 100644 index 00000000000..41fd4f880c9 --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone5.png diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png Binary files differnew file mode 100644 index 00000000000..5f7e9303f89 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png Binary files differnew file mode 100644 index 00000000000..7b6b3acd99b --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone1.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png Binary files differnew file mode 100644 index 00000000000..7975191b360 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone2.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png Binary files differnew file mode 100644 index 00000000000..a2816f600ae --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone3.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png Binary files differnew file mode 100644 index 00000000000..ea8291760f9 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone4.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png Binary files differnew file mode 100644 index 00000000000..c743e05fc5e --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone5.png diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png Binary files differnew file mode 100644 index 00000000000..bb86e963a80 --- /dev/null +++ b/app/assets/images/emoji/man_tone1.png diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png Binary files differnew file mode 100644 index 00000000000..fdeeaff46f5 --- /dev/null +++ b/app/assets/images/emoji/man_tone2.png diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png Binary files differnew file mode 100644 index 00000000000..7ae0b5df9cf --- /dev/null +++ b/app/assets/images/emoji/man_tone3.png diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png Binary files differnew file mode 100644 index 00000000000..db14cde99b8 --- /dev/null +++ b/app/assets/images/emoji/man_tone4.png diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png Binary files differnew file mode 100644 index 00000000000..7c67a70529c --- /dev/null +++ b/app/assets/images/emoji/man_tone5.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png Binary files differnew file mode 100644 index 00000000000..7841e13608d --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png Binary files differnew file mode 100644 index 00000000000..5b7b3def19c --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png Binary files differnew file mode 100644 index 00000000000..c8b9cf87f4b --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png Binary files differnew file mode 100644 index 00000000000..effdd0c4c84 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png Binary files differnew file mode 100644 index 00000000000..f885ff46fa1 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png Binary files differnew file mode 100644 index 00000000000..a6d55ca1380 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png Binary files differnew file mode 100644 index 00000000000..51cf047f966 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban.png diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png Binary files differnew file mode 100644 index 00000000000..1e12ee4b231 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone1.png diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png Binary files differnew file mode 100644 index 00000000000..37de4cceb23 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone2.png diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png Binary files differnew file mode 100644 index 00000000000..f607afd3450 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone3.png diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png Binary files differnew file mode 100644 index 00000000000..c05695888af --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone4.png diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png Binary files differnew file mode 100644 index 00000000000..4b4ff64720b --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone5.png diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png Binary files differnew file mode 100644 index 00000000000..4bf7541032c --- /dev/null +++ b/app/assets/images/emoji/mans_shoe.png diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png Binary files differnew file mode 100644 index 00000000000..15efe32c798 --- /dev/null +++ b/app/assets/images/emoji/map.png diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png Binary files differnew file mode 100644 index 00000000000..c49acea67f7 --- /dev/null +++ b/app/assets/images/emoji/maple_leaf.png diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png Binary files differnew file mode 100644 index 00000000000..8d6114761f6 --- /dev/null +++ b/app/assets/images/emoji/martial_arts_uniform.png diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png Binary files differnew file mode 100644 index 00000000000..1e800acd1c0 --- /dev/null +++ b/app/assets/images/emoji/mask.png diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png Binary files differnew file mode 100644 index 00000000000..b91d845e374 --- /dev/null +++ b/app/assets/images/emoji/massage.png diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png Binary files differnew file mode 100644 index 00000000000..e0f415d3186 --- /dev/null +++ b/app/assets/images/emoji/massage_tone1.png diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png Binary files differnew file mode 100644 index 00000000000..0bb244a270b --- /dev/null +++ b/app/assets/images/emoji/massage_tone2.png diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png Binary files differnew file mode 100644 index 00000000000..a117ee81a22 --- /dev/null +++ b/app/assets/images/emoji/massage_tone3.png diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png Binary files differnew file mode 100644 index 00000000000..6f42ab017f4 --- /dev/null +++ b/app/assets/images/emoji/massage_tone4.png diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png Binary files differnew file mode 100644 index 00000000000..6a388c0d0b5 --- /dev/null +++ b/app/assets/images/emoji/massage_tone5.png diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png Binary files differnew file mode 100644 index 00000000000..b20a59d1690 --- /dev/null +++ b/app/assets/images/emoji/meat_on_bone.png diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png Binary files differnew file mode 100644 index 00000000000..b85896b14da --- /dev/null +++ b/app/assets/images/emoji/medal.png diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png Binary files differnew file mode 100644 index 00000000000..4e6735188e3 --- /dev/null +++ b/app/assets/images/emoji/mega.png diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png Binary files differnew file mode 100644 index 00000000000..c01232d419d --- /dev/null +++ b/app/assets/images/emoji/melon.png diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png Binary files differnew file mode 100644 index 00000000000..b4297362869 --- /dev/null +++ b/app/assets/images/emoji/menorah.png diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png Binary files differnew file mode 100644 index 00000000000..f5a1e1ba0cd --- /dev/null +++ b/app/assets/images/emoji/mens.png diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png Binary files differnew file mode 100644 index 00000000000..4aa6e7e0a44 --- /dev/null +++ b/app/assets/images/emoji/metal.png diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png Binary files differnew file mode 100644 index 00000000000..c080d2addbd --- /dev/null +++ b/app/assets/images/emoji/metal_tone1.png diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png Binary files differnew file mode 100644 index 00000000000..12313529bcf --- /dev/null +++ b/app/assets/images/emoji/metal_tone2.png diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png Binary files differnew file mode 100644 index 00000000000..ca9be6ae67b --- /dev/null +++ b/app/assets/images/emoji/metal_tone3.png diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png Binary files differnew file mode 100644 index 00000000000..abe28cbf890 --- /dev/null +++ b/app/assets/images/emoji/metal_tone4.png diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png Binary files differnew file mode 100644 index 00000000000..0c6b5dd34ed --- /dev/null +++ b/app/assets/images/emoji/metal_tone5.png diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png Binary files differnew file mode 100644 index 00000000000..1de8f0551f3 --- /dev/null +++ b/app/assets/images/emoji/metro.png diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png Binary files differnew file mode 100644 index 00000000000..d4e6b0def25 --- /dev/null +++ b/app/assets/images/emoji/microphone.png diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png Binary files differnew file mode 100644 index 00000000000..cd9167654ff --- /dev/null +++ b/app/assets/images/emoji/microphone2.png diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png Binary files differnew file mode 100644 index 00000000000..90f5acf6a78 --- /dev/null +++ b/app/assets/images/emoji/microscope.png diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png Binary files differnew file mode 100644 index 00000000000..697f7a25eb2 --- /dev/null +++ b/app/assets/images/emoji/middle_finger.png diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png Binary files differnew file mode 100644 index 00000000000..61ef12a1548 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone1.png diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png Binary files differnew file mode 100644 index 00000000000..c31a69be9af --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone2.png diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png Binary files differnew file mode 100644 index 00000000000..73ac216ce63 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone3.png diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png Binary files differnew file mode 100644 index 00000000000..80b8ab7706d --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone4.png diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png Binary files differnew file mode 100644 index 00000000000..a8826b196e8 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone5.png diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png Binary files differnew file mode 100644 index 00000000000..ecd3fb03584 --- /dev/null +++ b/app/assets/images/emoji/military_medal.png diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png Binary files differnew file mode 100644 index 00000000000..e4fcf2e64f3 --- /dev/null +++ b/app/assets/images/emoji/milk.png diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png Binary files differnew file mode 100644 index 00000000000..b2b8ac59c5e --- /dev/null +++ b/app/assets/images/emoji/milky_way.png diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png Binary files differnew file mode 100644 index 00000000000..c60dd8f47ab --- /dev/null +++ b/app/assets/images/emoji/minibus.png diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png Binary files differnew file mode 100644 index 00000000000..9fa94cfbe74 --- /dev/null +++ b/app/assets/images/emoji/minidisc.png diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png Binary files differnew file mode 100644 index 00000000000..8b661ec1c94 --- /dev/null +++ b/app/assets/images/emoji/mobile_phone_off.png diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png Binary files differnew file mode 100644 index 00000000000..75fd1e90cb0 --- /dev/null +++ b/app/assets/images/emoji/money_mouth.png diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png Binary files differnew file mode 100644 index 00000000000..f022b04b3c2 --- /dev/null +++ b/app/assets/images/emoji/money_with_wings.png diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png Binary files differnew file mode 100644 index 00000000000..b9296be0902 --- /dev/null +++ b/app/assets/images/emoji/moneybag.png diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png Binary files differnew file mode 100644 index 00000000000..9fae29448e3 --- /dev/null +++ b/app/assets/images/emoji/monkey.png diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png Binary files differnew file mode 100644 index 00000000000..7cab9b91a82 --- /dev/null +++ b/app/assets/images/emoji/monkey_face.png diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png Binary files differnew file mode 100644 index 00000000000..11eb1f574bf --- /dev/null +++ b/app/assets/images/emoji/monorail.png diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png Binary files differnew file mode 100644 index 00000000000..8b17ddd9d00 --- /dev/null +++ b/app/assets/images/emoji/mortar_board.png diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png Binary files differnew file mode 100644 index 00000000000..ef770b26d96 --- /dev/null +++ b/app/assets/images/emoji/mosque.png diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png Binary files differnew file mode 100644 index 00000000000..c5afa72d807 --- /dev/null +++ b/app/assets/images/emoji/motor_scooter.png diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png Binary files differnew file mode 100644 index 00000000000..0506db1a40f --- /dev/null +++ b/app/assets/images/emoji/motorboat.png diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png Binary files differnew file mode 100644 index 00000000000..3d1d567e8ec --- /dev/null +++ b/app/assets/images/emoji/motorcycle.png diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png Binary files differnew file mode 100644 index 00000000000..8c3d3d03e3f --- /dev/null +++ b/app/assets/images/emoji/motorway.png diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png Binary files differnew file mode 100644 index 00000000000..88a54752458 --- /dev/null +++ b/app/assets/images/emoji/mount_fuji.png diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png Binary files differnew file mode 100644 index 00000000000..6722ebdd294 --- /dev/null +++ b/app/assets/images/emoji/mountain.png diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png Binary files differnew file mode 100644 index 00000000000..41d3dc3ac6f --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png Binary files differnew file mode 100644 index 00000000000..e9f1daf5e40 --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone1.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png Binary files differnew file mode 100644 index 00000000000..555b9e29d4d --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone2.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png Binary files differnew file mode 100644 index 00000000000..7df5508ec8c --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone3.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png Binary files differnew file mode 100644 index 00000000000..f94b3450697 --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone4.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png Binary files differnew file mode 100644 index 00000000000..16a45861e1f --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone5.png diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png Binary files differnew file mode 100644 index 00000000000..1dea73ca53b --- /dev/null +++ b/app/assets/images/emoji/mountain_cableway.png diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png Binary files differnew file mode 100644 index 00000000000..ade2218e469 --- /dev/null +++ b/app/assets/images/emoji/mountain_railway.png diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png Binary files differnew file mode 100644 index 00000000000..76e1cfd8313 --- /dev/null +++ b/app/assets/images/emoji/mountain_snow.png diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png Binary files differnew file mode 100644 index 00000000000..50afcd3262e --- /dev/null +++ b/app/assets/images/emoji/mouse.png diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png Binary files differnew file mode 100644 index 00000000000..20fb041f09f --- /dev/null +++ b/app/assets/images/emoji/mouse2.png diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png Binary files differnew file mode 100644 index 00000000000..e84e96ff6e8 --- /dev/null +++ b/app/assets/images/emoji/mouse_three_button.png diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png Binary files differnew file mode 100644 index 00000000000..4e73b130155 --- /dev/null +++ b/app/assets/images/emoji/movie_camera.png diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png Binary files differnew file mode 100644 index 00000000000..e6a7779c45b --- /dev/null +++ b/app/assets/images/emoji/moyai.png diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png Binary files differnew file mode 100644 index 00000000000..078f0657f95 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus.png diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png Binary files differnew file mode 100644 index 00000000000..d8a695d7035 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone1.png diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png Binary files differnew file mode 100644 index 00000000000..0e17e8c51f3 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone2.png diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png Binary files differnew file mode 100644 index 00000000000..c3ee4d1dfae --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone3.png diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png Binary files differnew file mode 100644 index 00000000000..68a556da2fe --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone4.png diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png Binary files differnew file mode 100644 index 00000000000..ccab3c40ff2 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone5.png diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png Binary files differnew file mode 100644 index 00000000000..7e67c1880f7 --- /dev/null +++ b/app/assets/images/emoji/muscle.png diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png Binary files differnew file mode 100644 index 00000000000..1522942ce51 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone1.png diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png Binary files differnew file mode 100644 index 00000000000..569c6e832ca --- /dev/null +++ b/app/assets/images/emoji/muscle_tone2.png diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png Binary files differnew file mode 100644 index 00000000000..0a76b00fa89 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone3.png diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png Binary files differnew file mode 100644 index 00000000000..f0cf31328e0 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone4.png diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png Binary files differnew file mode 100644 index 00000000000..4fda92460e8 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone5.png diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png Binary files differnew file mode 100644 index 00000000000..dd85742ba2c --- /dev/null +++ b/app/assets/images/emoji/mushroom.png diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png Binary files differnew file mode 100644 index 00000000000..442b7456842 --- /dev/null +++ b/app/assets/images/emoji/musical_keyboard.png diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png Binary files differnew file mode 100644 index 00000000000..06691ef61bb --- /dev/null +++ b/app/assets/images/emoji/musical_note.png diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png Binary files differnew file mode 100644 index 00000000000..47dc05a8ef5 --- /dev/null +++ b/app/assets/images/emoji/musical_score.png diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png Binary files differnew file mode 100644 index 00000000000..7c1788e5075 --- /dev/null +++ b/app/assets/images/emoji/mute.png diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png Binary files differnew file mode 100644 index 00000000000..aa52af7050d --- /dev/null +++ b/app/assets/images/emoji/nail_care.png diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png Binary files differnew file mode 100644 index 00000000000..26e883dd244 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone1.png diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png Binary files differnew file mode 100644 index 00000000000..61257b47ea3 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone2.png diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png Binary files differnew file mode 100644 index 00000000000..29871b05f62 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone3.png diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png Binary files differnew file mode 100644 index 00000000000..2881de0b17d --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone4.png diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png Binary files differnew file mode 100644 index 00000000000..a0b7c0a45a6 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone5.png diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png Binary files differnew file mode 100644 index 00000000000..ec5ee213e20 --- /dev/null +++ b/app/assets/images/emoji/name_badge.png diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png Binary files differnew file mode 100644 index 00000000000..a566c109c28 --- /dev/null +++ b/app/assets/images/emoji/nauseated_face.png diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png Binary files differnew file mode 100644 index 00000000000..1804e7f3ff3 --- /dev/null +++ b/app/assets/images/emoji/necktie.png diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png Binary files differnew file mode 100644 index 00000000000..dae487f1f98 --- /dev/null +++ b/app/assets/images/emoji/negative_squared_cross_mark.png diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png Binary files differnew file mode 100644 index 00000000000..7820bd581dc --- /dev/null +++ b/app/assets/images/emoji/nerd.png diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png Binary files differnew file mode 100644 index 00000000000..065d193afe4 --- /dev/null +++ b/app/assets/images/emoji/neutral_face.png diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png Binary files differnew file mode 100644 index 00000000000..b4f85488d1a --- /dev/null +++ b/app/assets/images/emoji/new.png diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png Binary files differnew file mode 100644 index 00000000000..ecff72caa42 --- /dev/null +++ b/app/assets/images/emoji/new_moon.png diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..150dd12400c --- /dev/null +++ b/app/assets/images/emoji/new_moon_with_face.png diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png Binary files differnew file mode 100644 index 00000000000..2aa8f060bde --- /dev/null +++ b/app/assets/images/emoji/newspaper.png diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png Binary files differnew file mode 100644 index 00000000000..f64748df2b2 --- /dev/null +++ b/app/assets/images/emoji/newspaper2.png diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png Binary files differnew file mode 100644 index 00000000000..ee8d20f5ebc --- /dev/null +++ b/app/assets/images/emoji/ng.png diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png Binary files differnew file mode 100644 index 00000000000..ca2018f456d --- /dev/null +++ b/app/assets/images/emoji/night_with_stars.png diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png Binary files differnew file mode 100644 index 00000000000..9fce3d1eca9 --- /dev/null +++ b/app/assets/images/emoji/nine.png diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png Binary files differnew file mode 100644 index 00000000000..15cb38dd1e7 --- /dev/null +++ b/app/assets/images/emoji/no_bell.png diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png Binary files differnew file mode 100644 index 00000000000..19c85421ce9 --- /dev/null +++ b/app/assets/images/emoji/no_bicycles.png diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png Binary files differnew file mode 100644 index 00000000000..476800fc5c6 --- /dev/null +++ b/app/assets/images/emoji/no_entry.png diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png Binary files differnew file mode 100644 index 00000000000..d2efd65e74b --- /dev/null +++ b/app/assets/images/emoji/no_entry_sign.png diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png Binary files differnew file mode 100644 index 00000000000..ed577100322 --- /dev/null +++ b/app/assets/images/emoji/no_good.png diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png Binary files differnew file mode 100644 index 00000000000..5c1a3cbb884 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone1.png diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png Binary files differnew file mode 100644 index 00000000000..80d8021f8fe --- /dev/null +++ b/app/assets/images/emoji/no_good_tone2.png diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png Binary files differnew file mode 100644 index 00000000000..635e6a00815 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone3.png diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png Binary files differnew file mode 100644 index 00000000000..b96e412a374 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone4.png diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png Binary files differnew file mode 100644 index 00000000000..9a7084afa0a --- /dev/null +++ b/app/assets/images/emoji/no_good_tone5.png diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png Binary files differnew file mode 100644 index 00000000000..7b1ae6ea579 --- /dev/null +++ b/app/assets/images/emoji/no_mobile_phones.png diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png Binary files differnew file mode 100644 index 00000000000..b642f6c1172 --- /dev/null +++ b/app/assets/images/emoji/no_mouth.png diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png Binary files differnew file mode 100644 index 00000000000..286aa577a23 --- /dev/null +++ b/app/assets/images/emoji/no_pedestrians.png diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png Binary files differnew file mode 100644 index 00000000000..586b8d29d05 --- /dev/null +++ b/app/assets/images/emoji/no_smoking.png diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png Binary files differnew file mode 100644 index 00000000000..827d4193f4e --- /dev/null +++ b/app/assets/images/emoji/non-potable_water.png diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png Binary files differnew file mode 100644 index 00000000000..2f04ac5f98f --- /dev/null +++ b/app/assets/images/emoji/nose.png diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png Binary files differnew file mode 100644 index 00000000000..8008d17506e --- /dev/null +++ b/app/assets/images/emoji/nose_tone1.png diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png Binary files differnew file mode 100644 index 00000000000..ac17f26e827 --- /dev/null +++ b/app/assets/images/emoji/nose_tone2.png diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png Binary files differnew file mode 100644 index 00000000000..d8b6cbe0f8e --- /dev/null +++ b/app/assets/images/emoji/nose_tone3.png diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png Binary files differnew file mode 100644 index 00000000000..004b2631e2e --- /dev/null +++ b/app/assets/images/emoji/nose_tone4.png diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png Binary files differnew file mode 100644 index 00000000000..7b33821f6c9 --- /dev/null +++ b/app/assets/images/emoji/nose_tone5.png diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png Binary files differnew file mode 100644 index 00000000000..f6c28b4915d --- /dev/null +++ b/app/assets/images/emoji/notebook.png diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png Binary files differnew file mode 100644 index 00000000000..03f566b6d2c --- /dev/null +++ b/app/assets/images/emoji/notebook_with_decorative_cover.png diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png Binary files differnew file mode 100644 index 00000000000..85faa10d8ea --- /dev/null +++ b/app/assets/images/emoji/notepad_spiral.png diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png Binary files differnew file mode 100644 index 00000000000..57d499aa181 --- /dev/null +++ b/app/assets/images/emoji/notes.png diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png Binary files differnew file mode 100644 index 00000000000..4b9ae155319 --- /dev/null +++ b/app/assets/images/emoji/nut_and_bolt.png diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png Binary files differnew file mode 100644 index 00000000000..3fe75ce4675 --- /dev/null +++ b/app/assets/images/emoji/o.png diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png Binary files differnew file mode 100644 index 00000000000..73278ba194a --- /dev/null +++ b/app/assets/images/emoji/o2.png diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png Binary files differnew file mode 100644 index 00000000000..45ff1e87703 --- /dev/null +++ b/app/assets/images/emoji/ocean.png diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png Binary files differnew file mode 100644 index 00000000000..5ed61004045 --- /dev/null +++ b/app/assets/images/emoji/octagonal_sign.png diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png Binary files differnew file mode 100644 index 00000000000..72c84074aac --- /dev/null +++ b/app/assets/images/emoji/octopus.png diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png Binary files differnew file mode 100644 index 00000000000..d38a849fece --- /dev/null +++ b/app/assets/images/emoji/oden.png diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png Binary files differnew file mode 100644 index 00000000000..7eee927d1b0 --- /dev/null +++ b/app/assets/images/emoji/office.png diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png Binary files differnew file mode 100644 index 00000000000..c4c4d42da8b --- /dev/null +++ b/app/assets/images/emoji/oil.png diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png Binary files differnew file mode 100644 index 00000000000..d0d775532ff --- /dev/null +++ b/app/assets/images/emoji/ok.png diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png Binary files differnew file mode 100644 index 00000000000..028d69b0de3 --- /dev/null +++ b/app/assets/images/emoji/ok_hand.png diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..cecf7b2ab5a --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone1.png diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..c19239bcd3d --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone2.png diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..94b65b03ecd --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone3.png diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..03d26f08e6a --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone4.png diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..d4b24086364 --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone5.png diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png Binary files differnew file mode 100644 index 00000000000..90a2c7469c4 --- /dev/null +++ b/app/assets/images/emoji/ok_woman.png diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..c99543e785b --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone1.png diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..ad5fae813db --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone2.png diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..51bf4fab406 --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone3.png diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..ee3f9dc640a --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone4.png diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..62a9d9237f7 --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone5.png diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png Binary files differnew file mode 100644 index 00000000000..4ace4e6f308 --- /dev/null +++ b/app/assets/images/emoji/older_man.png diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png Binary files differnew file mode 100644 index 00000000000..ab459baace8 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone1.png diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png Binary files differnew file mode 100644 index 00000000000..f4dfc7694ea --- /dev/null +++ b/app/assets/images/emoji/older_man_tone2.png diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png Binary files differnew file mode 100644 index 00000000000..5ffd11792f4 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone3.png diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png Binary files differnew file mode 100644 index 00000000000..b350a764bfd --- /dev/null +++ b/app/assets/images/emoji/older_man_tone4.png diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png Binary files differnew file mode 100644 index 00000000000..05fe24a1708 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone5.png diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png Binary files differnew file mode 100644 index 00000000000..52dc4987143 --- /dev/null +++ b/app/assets/images/emoji/older_woman.png diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..b49e821402c --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone1.png diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..e86bf5ab3b7 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone2.png diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..83fc14b0874 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone3.png diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..e4aa8a424d4 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone4.png diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..4009012bb0a --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone5.png diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png Binary files differnew file mode 100644 index 00000000000..a35c63c459c --- /dev/null +++ b/app/assets/images/emoji/om_symbol.png diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png Binary files differnew file mode 100644 index 00000000000..a0c371ae21e --- /dev/null +++ b/app/assets/images/emoji/on.png diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png Binary files differnew file mode 100644 index 00000000000..3c7e1d52e63 --- /dev/null +++ b/app/assets/images/emoji/oncoming_automobile.png diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png Binary files differnew file mode 100644 index 00000000000..ad91e256c7f --- /dev/null +++ b/app/assets/images/emoji/oncoming_bus.png diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png Binary files differnew file mode 100644 index 00000000000..c9109c85b5d --- /dev/null +++ b/app/assets/images/emoji/oncoming_police_car.png diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png Binary files differnew file mode 100644 index 00000000000..fea14e45846 --- /dev/null +++ b/app/assets/images/emoji/oncoming_taxi.png diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png Binary files differnew file mode 100644 index 00000000000..e6d84b80128 --- /dev/null +++ b/app/assets/images/emoji/one.png diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png Binary files differnew file mode 100644 index 00000000000..3993b09222f --- /dev/null +++ b/app/assets/images/emoji/open_file_folder.png diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png Binary files differnew file mode 100644 index 00000000000..1cf75c9101e --- /dev/null +++ b/app/assets/images/emoji/open_hands.png diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png Binary files differnew file mode 100644 index 00000000000..352d2614f11 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone1.png diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png Binary files differnew file mode 100644 index 00000000000..70824a50c73 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone2.png diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png Binary files differnew file mode 100644 index 00000000000..d7d136bd3db --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone3.png diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png Binary files differnew file mode 100644 index 00000000000..df4eaa711e7 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone4.png diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png Binary files differnew file mode 100644 index 00000000000..7dc04eaebd8 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone5.png diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png Binary files differnew file mode 100644 index 00000000000..a62cd27e148 --- /dev/null +++ b/app/assets/images/emoji/open_mouth.png diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png Binary files differnew file mode 100644 index 00000000000..0a780a700da --- /dev/null +++ b/app/assets/images/emoji/ophiuchus.png diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png Binary files differnew file mode 100644 index 00000000000..ab40e6ae6a2 --- /dev/null +++ b/app/assets/images/emoji/orange_book.png diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png Binary files differnew file mode 100644 index 00000000000..0530e33a4d4 --- /dev/null +++ b/app/assets/images/emoji/orthodox_cross.png diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png Binary files differnew file mode 100644 index 00000000000..46493ed5b2c --- /dev/null +++ b/app/assets/images/emoji/outbox_tray.png diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png Binary files differnew file mode 100644 index 00000000000..fa6815480c3 --- /dev/null +++ b/app/assets/images/emoji/owl.png diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png Binary files differnew file mode 100644 index 00000000000..badf5708f2f --- /dev/null +++ b/app/assets/images/emoji/ox.png diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png Binary files differnew file mode 100644 index 00000000000..85431756ad8 --- /dev/null +++ b/app/assets/images/emoji/package.png diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png Binary files differnew file mode 100644 index 00000000000..ba4ed757e01 --- /dev/null +++ b/app/assets/images/emoji/page_facing_up.png diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png Binary files differnew file mode 100644 index 00000000000..06355319c74 --- /dev/null +++ b/app/assets/images/emoji/page_with_curl.png diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png Binary files differnew file mode 100644 index 00000000000..b24b99306a2 --- /dev/null +++ b/app/assets/images/emoji/pager.png diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png Binary files differnew file mode 100644 index 00000000000..28bffbaa3c9 --- /dev/null +++ b/app/assets/images/emoji/paintbrush.png diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png Binary files differnew file mode 100644 index 00000000000..4bbb10f4f19 --- /dev/null +++ b/app/assets/images/emoji/palm_tree.png diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png Binary files differnew file mode 100644 index 00000000000..6223d1a28e9 --- /dev/null +++ b/app/assets/images/emoji/pancakes.png diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png Binary files differnew file mode 100644 index 00000000000..978382775ce --- /dev/null +++ b/app/assets/images/emoji/panda_face.png diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png Binary files differnew file mode 100644 index 00000000000..8cd8d4f8750 --- /dev/null +++ b/app/assets/images/emoji/paperclip.png diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png Binary files differnew file mode 100644 index 00000000000..76021e8c705 --- /dev/null +++ b/app/assets/images/emoji/paperclips.png diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png Binary files differnew file mode 100644 index 00000000000..63ec7016301 --- /dev/null +++ b/app/assets/images/emoji/park.png diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png Binary files differnew file mode 100644 index 00000000000..7be7dac27e8 --- /dev/null +++ b/app/assets/images/emoji/parking.png diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png Binary files differnew file mode 100644 index 00000000000..70453d41528 --- /dev/null +++ b/app/assets/images/emoji/part_alternation_mark.png diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png Binary files differnew file mode 100644 index 00000000000..a55e59c344c --- /dev/null +++ b/app/assets/images/emoji/partly_sunny.png diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png Binary files differnew file mode 100644 index 00000000000..079e34ee4d4 --- /dev/null +++ b/app/assets/images/emoji/passport_control.png diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png Binary files differnew file mode 100644 index 00000000000..4f07e7ebfd7 --- /dev/null +++ b/app/assets/images/emoji/pause_button.png diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png Binary files differnew file mode 100644 index 00000000000..86033faf477 --- /dev/null +++ b/app/assets/images/emoji/peace.png diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png Binary files differnew file mode 100644 index 00000000000..9ab57cbb758 --- /dev/null +++ b/app/assets/images/emoji/peach.png diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png Binary files differnew file mode 100644 index 00000000000..b64fadad010 --- /dev/null +++ b/app/assets/images/emoji/peanuts.png diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png Binary files differnew file mode 100644 index 00000000000..3869f718bcf --- /dev/null +++ b/app/assets/images/emoji/pear.png diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png Binary files differnew file mode 100644 index 00000000000..6ef7a342433 --- /dev/null +++ b/app/assets/images/emoji/pen_ballpoint.png diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png Binary files differnew file mode 100644 index 00000000000..3ca4bd2c231 --- /dev/null +++ b/app/assets/images/emoji/pen_fountain.png diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png Binary files differnew file mode 100644 index 00000000000..edc6155e168 --- /dev/null +++ b/app/assets/images/emoji/pencil.png diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png Binary files differnew file mode 100644 index 00000000000..3833d590fa2 --- /dev/null +++ b/app/assets/images/emoji/pencil2.png diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png Binary files differnew file mode 100644 index 00000000000..c0064fb9734 --- /dev/null +++ b/app/assets/images/emoji/penguin.png diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png Binary files differnew file mode 100644 index 00000000000..490fb566954 --- /dev/null +++ b/app/assets/images/emoji/pensive.png diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png Binary files differnew file mode 100644 index 00000000000..685441fdaa1 --- /dev/null +++ b/app/assets/images/emoji/performing_arts.png diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png Binary files differnew file mode 100644 index 00000000000..646a05fe908 --- /dev/null +++ b/app/assets/images/emoji/persevere.png diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png Binary files differnew file mode 100644 index 00000000000..579324959a1 --- /dev/null +++ b/app/assets/images/emoji/person_frowning.png diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png Binary files differnew file mode 100644 index 00000000000..21d3bb43923 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone1.png diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png Binary files differnew file mode 100644 index 00000000000..973f5fc8382 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone2.png diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png Binary files differnew file mode 100644 index 00000000000..41fbcc78816 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone3.png diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png Binary files differnew file mode 100644 index 00000000000..5a37c741030 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone4.png diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png Binary files differnew file mode 100644 index 00000000000..e08141f3efe --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone5.png diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png Binary files differnew file mode 100644 index 00000000000..ad6f01a7dda --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png Binary files differnew file mode 100644 index 00000000000..7d18ef24445 --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone1.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png Binary files differnew file mode 100644 index 00000000000..dae1307315c --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone2.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png Binary files differnew file mode 100644 index 00000000000..684677e8e5a --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone3.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png Binary files differnew file mode 100644 index 00000000000..012be0b51f8 --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone4.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png Binary files differnew file mode 100644 index 00000000000..d4ecc4cf44b --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone5.png diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png Binary files differnew file mode 100644 index 00000000000..10eb0571078 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png Binary files differnew file mode 100644 index 00000000000..57e826b75a4 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone1.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png Binary files differnew file mode 100644 index 00000000000..3f317c0c25f --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone2.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png Binary files differnew file mode 100644 index 00000000000..d2fbb6c20bf --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone3.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png Binary files differnew file mode 100644 index 00000000000..643ceb4a5c5 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone4.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png Binary files differnew file mode 100644 index 00000000000..b2eb6859c32 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone5.png diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png Binary files differnew file mode 100644 index 00000000000..6370fe6d791 --- /dev/null +++ b/app/assets/images/emoji/pick.png diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png Binary files differnew file mode 100644 index 00000000000..afe05ca1676 --- /dev/null +++ b/app/assets/images/emoji/pig.png diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png Binary files differnew file mode 100644 index 00000000000..5f31c1a2d75 --- /dev/null +++ b/app/assets/images/emoji/pig2.png diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png Binary files differnew file mode 100644 index 00000000000..3610ae4a910 --- /dev/null +++ b/app/assets/images/emoji/pig_nose.png diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png Binary files differnew file mode 100644 index 00000000000..1d4530e77a3 --- /dev/null +++ b/app/assets/images/emoji/pill.png diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png Binary files differnew file mode 100644 index 00000000000..c89a1606462 --- /dev/null +++ b/app/assets/images/emoji/pineapple.png diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png Binary files differnew file mode 100644 index 00000000000..ff3c51727d1 --- /dev/null +++ b/app/assets/images/emoji/ping_pong.png diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png Binary files differnew file mode 100644 index 00000000000..7f6f646a95c --- /dev/null +++ b/app/assets/images/emoji/pisces.png diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png Binary files differnew file mode 100644 index 00000000000..e07365cb398 --- /dev/null +++ b/app/assets/images/emoji/pizza.png diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png Binary files differnew file mode 100644 index 00000000000..207d59cce85 --- /dev/null +++ b/app/assets/images/emoji/place_of_worship.png diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png Binary files differnew file mode 100644 index 00000000000..a9f857139ac --- /dev/null +++ b/app/assets/images/emoji/play_pause.png diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png Binary files differnew file mode 100644 index 00000000000..00d3d13ab5c --- /dev/null +++ b/app/assets/images/emoji/point_down.png diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png Binary files differnew file mode 100644 index 00000000000..140f157d8c7 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone1.png diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png Binary files differnew file mode 100644 index 00000000000..d518544f7fa --- /dev/null +++ b/app/assets/images/emoji/point_down_tone2.png diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png Binary files differnew file mode 100644 index 00000000000..018b688b8b7 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone3.png diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png Binary files differnew file mode 100644 index 00000000000..98845bf6f72 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone4.png diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png Binary files differnew file mode 100644 index 00000000000..9a9b039a9fc --- /dev/null +++ b/app/assets/images/emoji/point_down_tone5.png diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png Binary files differnew file mode 100644 index 00000000000..599fa2e3cf1 --- /dev/null +++ b/app/assets/images/emoji/point_left.png diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png Binary files differnew file mode 100644 index 00000000000..88e2c306076 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone1.png diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png Binary files differnew file mode 100644 index 00000000000..d3c89d87c5f --- /dev/null +++ b/app/assets/images/emoji/point_left_tone2.png diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png Binary files differnew file mode 100644 index 00000000000..b23b9167358 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone3.png diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png Binary files differnew file mode 100644 index 00000000000..3093f325c27 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone4.png diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png Binary files differnew file mode 100644 index 00000000000..2b4cbfa120c --- /dev/null +++ b/app/assets/images/emoji/point_left_tone5.png diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png Binary files differnew file mode 100644 index 00000000000..93a3cd34aa5 --- /dev/null +++ b/app/assets/images/emoji/point_right.png diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png Binary files differnew file mode 100644 index 00000000000..4a28c6bbc89 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone1.png diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png Binary files differnew file mode 100644 index 00000000000..7cb13231733 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone2.png diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png Binary files differnew file mode 100644 index 00000000000..5514807d71a --- /dev/null +++ b/app/assets/images/emoji/point_right_tone3.png diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png Binary files differnew file mode 100644 index 00000000000..b8541d6440d --- /dev/null +++ b/app/assets/images/emoji/point_right_tone4.png diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png Binary files differnew file mode 100644 index 00000000000..1b7aab07bb1 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone5.png diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png Binary files differnew file mode 100644 index 00000000000..f4978ff0f00 --- /dev/null +++ b/app/assets/images/emoji/point_up.png diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png Binary files differnew file mode 100644 index 00000000000..bc496dfeae4 --- /dev/null +++ b/app/assets/images/emoji/point_up_2.png diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png Binary files differnew file mode 100644 index 00000000000..a12a7e78430 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone1.png diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png Binary files differnew file mode 100644 index 00000000000..cdff40ceab0 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone2.png diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png Binary files differnew file mode 100644 index 00000000000..a07ce9e5ae8 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone3.png diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png Binary files differnew file mode 100644 index 00000000000..4f86c88ba42 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone4.png diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png Binary files differnew file mode 100644 index 00000000000..ed1b26c35d3 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone5.png diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png Binary files differnew file mode 100644 index 00000000000..6a9db21d64c --- /dev/null +++ b/app/assets/images/emoji/point_up_tone1.png diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png Binary files differnew file mode 100644 index 00000000000..15aa9ea0e05 --- /dev/null +++ b/app/assets/images/emoji/point_up_tone2.png diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png Binary files differnew file mode 100644 index 00000000000..652b73a9c5d --- /dev/null +++ b/app/assets/images/emoji/point_up_tone3.png diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png Binary files differnew file mode 100644 index 00000000000..692bad926e9 --- /dev/null +++ b/app/assets/images/emoji/point_up_tone4.png diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png Binary files differnew file mode 100644 index 00000000000..1e1b10fb71c --- /dev/null +++ b/app/assets/images/emoji/point_up_tone5.png diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png Binary files differnew file mode 100644 index 00000000000..3da4253de7e --- /dev/null +++ b/app/assets/images/emoji/police_car.png diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png Binary files differnew file mode 100644 index 00000000000..8ec39e396af --- /dev/null +++ b/app/assets/images/emoji/poodle.png diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png Binary files differnew file mode 100644 index 00000000000..10b15e72d56 --- /dev/null +++ b/app/assets/images/emoji/poop.png diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png Binary files differnew file mode 100644 index 00000000000..36853e381d4 --- /dev/null +++ b/app/assets/images/emoji/popcorn.png diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png Binary files differnew file mode 100644 index 00000000000..a23848f9aa0 --- /dev/null +++ b/app/assets/images/emoji/post_office.png diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png Binary files differnew file mode 100644 index 00000000000..c173b8dbd67 --- /dev/null +++ b/app/assets/images/emoji/postal_horn.png diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png Binary files differnew file mode 100644 index 00000000000..07c9c4ab3d6 --- /dev/null +++ b/app/assets/images/emoji/postbox.png diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png Binary files differnew file mode 100644 index 00000000000..2c610049459 --- /dev/null +++ b/app/assets/images/emoji/potable_water.png diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png Binary files differnew file mode 100644 index 00000000000..70350ca2c0a --- /dev/null +++ b/app/assets/images/emoji/potato.png diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png Binary files differnew file mode 100644 index 00000000000..8795c6c66ff --- /dev/null +++ b/app/assets/images/emoji/pouch.png diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png Binary files differnew file mode 100644 index 00000000000..eea4a53a2f9 --- /dev/null +++ b/app/assets/images/emoji/poultry_leg.png diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png Binary files differnew file mode 100644 index 00000000000..a0d4c4099e9 --- /dev/null +++ b/app/assets/images/emoji/pound.png diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png Binary files differnew file mode 100644 index 00000000000..41ddfeab42b --- /dev/null +++ b/app/assets/images/emoji/pouting_cat.png diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png Binary files differnew file mode 100644 index 00000000000..8347f2435be --- /dev/null +++ b/app/assets/images/emoji/pray.png diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png Binary files differnew file mode 100644 index 00000000000..060ef257172 --- /dev/null +++ b/app/assets/images/emoji/pray_tone1.png diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png Binary files differnew file mode 100644 index 00000000000..56dc607c07a --- /dev/null +++ b/app/assets/images/emoji/pray_tone2.png diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png Binary files differnew file mode 100644 index 00000000000..0f33b862008 --- /dev/null +++ b/app/assets/images/emoji/pray_tone3.png diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png Binary files differnew file mode 100644 index 00000000000..2ea8dc11657 --- /dev/null +++ b/app/assets/images/emoji/pray_tone4.png diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png Binary files differnew file mode 100644 index 00000000000..2128a6c4703 --- /dev/null +++ b/app/assets/images/emoji/pray_tone5.png diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png Binary files differnew file mode 100644 index 00000000000..a4b6dfcc62e --- /dev/null +++ b/app/assets/images/emoji/prayer_beads.png diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png Binary files differnew file mode 100644 index 00000000000..084e83a414a --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman.png diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..a78703b33aa --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone1.png diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..0068c6c4a77 --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone2.png diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..3206296b684 --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone3.png diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..120fda5cd8c --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone4.png diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..569bfdf05ce --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone5.png diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png Binary files differnew file mode 100644 index 00000000000..38d69344c84 --- /dev/null +++ b/app/assets/images/emoji/prince.png diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png Binary files differnew file mode 100644 index 00000000000..849930c8887 --- /dev/null +++ b/app/assets/images/emoji/prince_tone1.png diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png Binary files differnew file mode 100644 index 00000000000..23d8b3b1285 --- /dev/null +++ b/app/assets/images/emoji/prince_tone2.png diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png Binary files differnew file mode 100644 index 00000000000..db6dfff0647 --- /dev/null +++ b/app/assets/images/emoji/prince_tone3.png diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png Binary files differnew file mode 100644 index 00000000000..8e10f8be6a8 --- /dev/null +++ b/app/assets/images/emoji/prince_tone4.png diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png Binary files differnew file mode 100644 index 00000000000..138d4ea7048 --- /dev/null +++ b/app/assets/images/emoji/prince_tone5.png diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png Binary files differnew file mode 100644 index 00000000000..879e9fa8c5d --- /dev/null +++ b/app/assets/images/emoji/princess.png diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png Binary files differnew file mode 100644 index 00000000000..c28078cdc36 --- /dev/null +++ b/app/assets/images/emoji/princess_tone1.png diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png Binary files differnew file mode 100644 index 00000000000..dcd20e6ecd4 --- /dev/null +++ b/app/assets/images/emoji/princess_tone2.png diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png Binary files differnew file mode 100644 index 00000000000..cde6f315c56 --- /dev/null +++ b/app/assets/images/emoji/princess_tone3.png diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png Binary files differnew file mode 100644 index 00000000000..c71e69caaef --- /dev/null +++ b/app/assets/images/emoji/princess_tone4.png diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png Binary files differnew file mode 100644 index 00000000000..063e2645910 --- /dev/null +++ b/app/assets/images/emoji/princess_tone5.png diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png Binary files differnew file mode 100644 index 00000000000..027c830f0fe --- /dev/null +++ b/app/assets/images/emoji/printer.png diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png Binary files differnew file mode 100644 index 00000000000..ce9ab0daa28 --- /dev/null +++ b/app/assets/images/emoji/projector.png diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png Binary files differnew file mode 100644 index 00000000000..b14ca5f5211 --- /dev/null +++ b/app/assets/images/emoji/punch.png diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png Binary files differnew file mode 100644 index 00000000000..93c7d17fb47 --- /dev/null +++ b/app/assets/images/emoji/punch_tone1.png diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png Binary files differnew file mode 100644 index 00000000000..c0a1af6e10a --- /dev/null +++ b/app/assets/images/emoji/punch_tone2.png diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png Binary files differnew file mode 100644 index 00000000000..1458b021201 --- /dev/null +++ b/app/assets/images/emoji/punch_tone3.png diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png Binary files differnew file mode 100644 index 00000000000..c1466bfcdef --- /dev/null +++ b/app/assets/images/emoji/punch_tone4.png diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png Binary files differnew file mode 100644 index 00000000000..00b4ddb8953 --- /dev/null +++ b/app/assets/images/emoji/punch_tone5.png diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png Binary files differnew file mode 100644 index 00000000000..95c53a9ade6 --- /dev/null +++ b/app/assets/images/emoji/purple_heart.png diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png Binary files differnew file mode 100644 index 00000000000..981346193c5 --- /dev/null +++ b/app/assets/images/emoji/purse.png diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png Binary files differnew file mode 100644 index 00000000000..57e07d7f4cc --- /dev/null +++ b/app/assets/images/emoji/pushpin.png diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png Binary files differnew file mode 100644 index 00000000000..82a84f9a375 --- /dev/null +++ b/app/assets/images/emoji/put_litter_in_its_place.png diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png Binary files differnew file mode 100644 index 00000000000..5a58f3458aa --- /dev/null +++ b/app/assets/images/emoji/question.png diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png Binary files differnew file mode 100644 index 00000000000..ea75ab0426e --- /dev/null +++ b/app/assets/images/emoji/rabbit.png diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png Binary files differnew file mode 100644 index 00000000000..2c8a29c642f --- /dev/null +++ b/app/assets/images/emoji/rabbit2.png diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png Binary files differnew file mode 100644 index 00000000000..fe3f045f446 --- /dev/null +++ b/app/assets/images/emoji/race_car.png diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png Binary files differnew file mode 100644 index 00000000000..b3e73cc8903 --- /dev/null +++ b/app/assets/images/emoji/racehorse.png diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png Binary files differnew file mode 100644 index 00000000000..dec381fa242 --- /dev/null +++ b/app/assets/images/emoji/radio.png diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png Binary files differnew file mode 100644 index 00000000000..3a23449d917 --- /dev/null +++ b/app/assets/images/emoji/radio_button.png diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png Binary files differnew file mode 100644 index 00000000000..3b46199fe37 --- /dev/null +++ b/app/assets/images/emoji/radioactive.png diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png Binary files differnew file mode 100644 index 00000000000..9d739bd40ad --- /dev/null +++ b/app/assets/images/emoji/rage.png diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png Binary files differnew file mode 100644 index 00000000000..a9acbf13008 --- /dev/null +++ b/app/assets/images/emoji/railway_car.png diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png Binary files differnew file mode 100644 index 00000000000..e1a7a0d1430 --- /dev/null +++ b/app/assets/images/emoji/railway_track.png diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png Binary files differnew file mode 100644 index 00000000000..154735d7147 --- /dev/null +++ b/app/assets/images/emoji/rainbow.png diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png Binary files differnew file mode 100644 index 00000000000..479234294b4 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..813d28499b5 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone1.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..192ff795e37 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone2.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..61a727abe6b --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone3.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..2e83da511f5 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone4.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..d7a5b95a02c --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone5.png diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png Binary files differnew file mode 100644 index 00000000000..6b2954315d1 --- /dev/null +++ b/app/assets/images/emoji/raised_hand.png diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..3b752902c07 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone1.png diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..44e2a514c60 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone2.png diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..5bb62a7528a --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone3.png diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..c7f8c9ec270 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone4.png diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..c601b58a73e --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone5.png diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png Binary files differnew file mode 100644 index 00000000000..c0155f728e7 --- /dev/null +++ b/app/assets/images/emoji/raised_hands.png diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png Binary files differnew file mode 100644 index 00000000000..1168b8236b6 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone1.png diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png Binary files differnew file mode 100644 index 00000000000..322de622903 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone2.png diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png Binary files differnew file mode 100644 index 00000000000..2aa24e05ae1 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone3.png diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png Binary files differnew file mode 100644 index 00000000000..f31bf0db992 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone4.png diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png Binary files differnew file mode 100644 index 00000000000..5e95067f98b --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone5.png diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png Binary files differnew file mode 100644 index 00000000000..2880708c0cc --- /dev/null +++ b/app/assets/images/emoji/raising_hand.png diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..1c90e3e2689 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone1.png diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..82c3ef2bfc5 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone2.png diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..1b1da2aa0ca --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone3.png diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..e453855c01f --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone4.png diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..b86200fd844 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone5.png diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png Binary files differnew file mode 100644 index 00000000000..52a44464c9b --- /dev/null +++ b/app/assets/images/emoji/ram.png diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png Binary files differnew file mode 100644 index 00000000000..c1cb7cd7384 --- /dev/null +++ b/app/assets/images/emoji/ramen.png diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png Binary files differnew file mode 100644 index 00000000000..86219144f10 --- /dev/null +++ b/app/assets/images/emoji/rat.png diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png Binary files differnew file mode 100644 index 00000000000..ada52830fce --- /dev/null +++ b/app/assets/images/emoji/record_button.png diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png Binary files differnew file mode 100644 index 00000000000..9221f095c37 --- /dev/null +++ b/app/assets/images/emoji/recycle.png diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png Binary files differnew file mode 100644 index 00000000000..b3e6a774dea --- /dev/null +++ b/app/assets/images/emoji/red_car.png diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png Binary files differnew file mode 100644 index 00000000000..4bef930d92f --- /dev/null +++ b/app/assets/images/emoji/red_circle.png diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png Binary files differnew file mode 100644 index 00000000000..53ef9f2d4e6 --- /dev/null +++ b/app/assets/images/emoji/registered.png diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png Binary files differnew file mode 100644 index 00000000000..e9e53c03d45 --- /dev/null +++ b/app/assets/images/emoji/relaxed.png diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png Binary files differnew file mode 100644 index 00000000000..715ad0bf53f --- /dev/null +++ b/app/assets/images/emoji/relieved.png diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png Binary files differnew file mode 100644 index 00000000000..3988bbd094c --- /dev/null +++ b/app/assets/images/emoji/reminder_ribbon.png diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png Binary files differnew file mode 100644 index 00000000000..540ce4e0fba --- /dev/null +++ b/app/assets/images/emoji/repeat.png diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png Binary files differnew file mode 100644 index 00000000000..9567e83337f --- /dev/null +++ b/app/assets/images/emoji/repeat_one.png diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png Binary files differnew file mode 100644 index 00000000000..9588e0f0ef7 --- /dev/null +++ b/app/assets/images/emoji/restroom.png diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png Binary files differnew file mode 100644 index 00000000000..7b9d1948f73 --- /dev/null +++ b/app/assets/images/emoji/revolving_hearts.png diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png Binary files differnew file mode 100644 index 00000000000..e22e2bd3da5 --- /dev/null +++ b/app/assets/images/emoji/rewind.png diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png Binary files differnew file mode 100644 index 00000000000..12f4e0d9d9b --- /dev/null +++ b/app/assets/images/emoji/rhino.png diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png Binary files differnew file mode 100644 index 00000000000..0f253c3d8c8 --- /dev/null +++ b/app/assets/images/emoji/ribbon.png diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png Binary files differnew file mode 100644 index 00000000000..6e3ac7956b1 --- /dev/null +++ b/app/assets/images/emoji/rice.png diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png Binary files differnew file mode 100644 index 00000000000..d3d8ee25cb8 --- /dev/null +++ b/app/assets/images/emoji/rice_ball.png diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png Binary files differnew file mode 100644 index 00000000000..7fbd08e4ff9 --- /dev/null +++ b/app/assets/images/emoji/rice_cracker.png diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png Binary files differnew file mode 100644 index 00000000000..1a28426592a --- /dev/null +++ b/app/assets/images/emoji/rice_scene.png diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png Binary files differnew file mode 100644 index 00000000000..754ed066d2c --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist.png diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png Binary files differnew file mode 100644 index 00000000000..33ded2f61a6 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone1.png diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png Binary files differnew file mode 100644 index 00000000000..88054e335c7 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone2.png diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png Binary files differnew file mode 100644 index 00000000000..84b9f5da7f7 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone3.png diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png Binary files differnew file mode 100644 index 00000000000..e741cfea68b --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone4.png diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png Binary files differnew file mode 100644 index 00000000000..cf66d760c1f --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone5.png diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png Binary files differnew file mode 100644 index 00000000000..87d227adb74 --- /dev/null +++ b/app/assets/images/emoji/ring.png diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png Binary files differnew file mode 100644 index 00000000000..7cc62612c6a --- /dev/null +++ b/app/assets/images/emoji/robot.png diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png Binary files differnew file mode 100644 index 00000000000..0d8da089a37 --- /dev/null +++ b/app/assets/images/emoji/rocket.png diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png Binary files differnew file mode 100644 index 00000000000..b1736fedfeb --- /dev/null +++ b/app/assets/images/emoji/rofl.png diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png Binary files differnew file mode 100644 index 00000000000..5b849e071e8 --- /dev/null +++ b/app/assets/images/emoji/roller_coaster.png diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png Binary files differnew file mode 100644 index 00000000000..2f77b9fc3b9 --- /dev/null +++ b/app/assets/images/emoji/rolling_eyes.png diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png Binary files differnew file mode 100644 index 00000000000..bbf2bbff97a --- /dev/null +++ b/app/assets/images/emoji/rooster.png diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png Binary files differnew file mode 100644 index 00000000000..52c286d31ce --- /dev/null +++ b/app/assets/images/emoji/rose.png diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png Binary files differnew file mode 100644 index 00000000000..8030e494bcf --- /dev/null +++ b/app/assets/images/emoji/rosette.png diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png Binary files differnew file mode 100644 index 00000000000..cad66b0afef --- /dev/null +++ b/app/assets/images/emoji/rotating_light.png diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png Binary files differnew file mode 100644 index 00000000000..28b9d72866e --- /dev/null +++ b/app/assets/images/emoji/round_pushpin.png diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png Binary files differnew file mode 100644 index 00000000000..dd4dfc095d9 --- /dev/null +++ b/app/assets/images/emoji/rowboat.png diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png Binary files differnew file mode 100644 index 00000000000..5e5d18548cb --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone1.png diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png Binary files differnew file mode 100644 index 00000000000..9b123ef8871 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone2.png diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png Binary files differnew file mode 100644 index 00000000000..8ebd89a55f5 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone3.png diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png Binary files differnew file mode 100644 index 00000000000..2b0d04f8725 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone4.png diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png Binary files differnew file mode 100644 index 00000000000..b346f2dfc84 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone5.png diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png Binary files differnew file mode 100644 index 00000000000..b1872273436 --- /dev/null +++ b/app/assets/images/emoji/rugby_football.png diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png Binary files differnew file mode 100644 index 00000000000..e914915976a --- /dev/null +++ b/app/assets/images/emoji/runner.png diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png Binary files differnew file mode 100644 index 00000000000..9355239a52d --- /dev/null +++ b/app/assets/images/emoji/runner_tone1.png diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png Binary files differnew file mode 100644 index 00000000000..6112fd5c376 --- /dev/null +++ b/app/assets/images/emoji/runner_tone2.png diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png Binary files differnew file mode 100644 index 00000000000..625ec708f48 --- /dev/null +++ b/app/assets/images/emoji/runner_tone3.png diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png Binary files differnew file mode 100644 index 00000000000..242f1b56337 --- /dev/null +++ b/app/assets/images/emoji/runner_tone4.png diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png Binary files differnew file mode 100644 index 00000000000..2976c6f019f --- /dev/null +++ b/app/assets/images/emoji/runner_tone5.png diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png Binary files differnew file mode 100644 index 00000000000..6d83c06b803 --- /dev/null +++ b/app/assets/images/emoji/running_shirt_with_sash.png diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png Binary files differnew file mode 100644 index 00000000000..900f9633247 --- /dev/null +++ b/app/assets/images/emoji/sa.png diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png Binary files differnew file mode 100644 index 00000000000..f8d94ff2923 --- /dev/null +++ b/app/assets/images/emoji/sagittarius.png diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png Binary files differnew file mode 100644 index 00000000000..772ef11da5d --- /dev/null +++ b/app/assets/images/emoji/sailboat.png diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png Binary files differnew file mode 100644 index 00000000000..2933f5672c4 --- /dev/null +++ b/app/assets/images/emoji/sake.png diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png Binary files differnew file mode 100644 index 00000000000..c89f9341158 --- /dev/null +++ b/app/assets/images/emoji/salad.png diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png Binary files differnew file mode 100644 index 00000000000..9d9f5122b7a --- /dev/null +++ b/app/assets/images/emoji/sandal.png diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png Binary files differnew file mode 100644 index 00000000000..bc83ab80d52 --- /dev/null +++ b/app/assets/images/emoji/santa.png diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png Binary files differnew file mode 100644 index 00000000000..5233ffb7174 --- /dev/null +++ b/app/assets/images/emoji/santa_tone1.png diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png Binary files differnew file mode 100644 index 00000000000..4e845438197 --- /dev/null +++ b/app/assets/images/emoji/santa_tone2.png diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png Binary files differnew file mode 100644 index 00000000000..7fc4f33b60f --- /dev/null +++ b/app/assets/images/emoji/santa_tone3.png diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png Binary files differnew file mode 100644 index 00000000000..d1d5a15132d --- /dev/null +++ b/app/assets/images/emoji/santa_tone4.png diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png Binary files differnew file mode 100644 index 00000000000..4d697a01f24 --- /dev/null +++ b/app/assets/images/emoji/santa_tone5.png diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png Binary files differnew file mode 100644 index 00000000000..db0372795f4 --- /dev/null +++ b/app/assets/images/emoji/satellite.png diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png Binary files differnew file mode 100644 index 00000000000..4ba55d6e297 --- /dev/null +++ b/app/assets/images/emoji/satellite_orbital.png diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png Binary files differnew file mode 100644 index 00000000000..a392faec291 --- /dev/null +++ b/app/assets/images/emoji/saxophone.png diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png Binary files differnew file mode 100644 index 00000000000..0757eda1684 --- /dev/null +++ b/app/assets/images/emoji/scales.png diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png Binary files differnew file mode 100644 index 00000000000..269759534f0 --- /dev/null +++ b/app/assets/images/emoji/school.png diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png Binary files differnew file mode 100644 index 00000000000..9997c86e7dc --- /dev/null +++ b/app/assets/images/emoji/school_satchel.png diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png Binary files differnew file mode 100644 index 00000000000..270571c8cdd --- /dev/null +++ b/app/assets/images/emoji/scissors.png diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png Binary files differnew file mode 100644 index 00000000000..4ab7ef59cd2 --- /dev/null +++ b/app/assets/images/emoji/scooter.png diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png Binary files differnew file mode 100644 index 00000000000..449a6b281c9 --- /dev/null +++ b/app/assets/images/emoji/scorpion.png diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png Binary files differnew file mode 100644 index 00000000000..c31a9920455 --- /dev/null +++ b/app/assets/images/emoji/scorpius.png diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png Binary files differnew file mode 100644 index 00000000000..c3bea9f2510 --- /dev/null +++ b/app/assets/images/emoji/scream.png diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png Binary files differnew file mode 100644 index 00000000000..15803ad8e6e --- /dev/null +++ b/app/assets/images/emoji/scream_cat.png diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png Binary files differnew file mode 100644 index 00000000000..50ee5dcd4b9 --- /dev/null +++ b/app/assets/images/emoji/scroll.png diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png Binary files differnew file mode 100644 index 00000000000..a6d72d95adb --- /dev/null +++ b/app/assets/images/emoji/seat.png diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png Binary files differnew file mode 100644 index 00000000000..17b011268b6 --- /dev/null +++ b/app/assets/images/emoji/second_place.png diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png Binary files differnew file mode 100644 index 00000000000..5fd72608e60 --- /dev/null +++ b/app/assets/images/emoji/secret.png diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png Binary files differnew file mode 100644 index 00000000000..5187e474531 --- /dev/null +++ b/app/assets/images/emoji/see_no_evil.png diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png Binary files differnew file mode 100644 index 00000000000..ae0948bcfd6 --- /dev/null +++ b/app/assets/images/emoji/seedling.png diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png Binary files differnew file mode 100644 index 00000000000..6a1ba75c7e3 --- /dev/null +++ b/app/assets/images/emoji/selfie.png diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png Binary files differnew file mode 100644 index 00000000000..290e075b56f --- /dev/null +++ b/app/assets/images/emoji/selfie_tone1.png diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png Binary files differnew file mode 100644 index 00000000000..fcd9595b643 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone2.png diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png Binary files differnew file mode 100644 index 00000000000..f3a22fdf435 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone3.png diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png Binary files differnew file mode 100644 index 00000000000..cdecf6d9f4e --- /dev/null +++ b/app/assets/images/emoji/selfie_tone4.png diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png Binary files differnew file mode 100644 index 00000000000..86acbb6c202 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone5.png diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png Binary files differnew file mode 100644 index 00000000000..9b3476ae7c7 --- /dev/null +++ b/app/assets/images/emoji/seven.png diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png Binary files differnew file mode 100644 index 00000000000..663a1006acd --- /dev/null +++ b/app/assets/images/emoji/shallow_pan_of_food.png diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png Binary files differnew file mode 100644 index 00000000000..f202aecfe6f --- /dev/null +++ b/app/assets/images/emoji/shamrock.png diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png Binary files differnew file mode 100644 index 00000000000..c75076d57d8 --- /dev/null +++ b/app/assets/images/emoji/shark.png diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png Binary files differnew file mode 100644 index 00000000000..36dfb53ca93 --- /dev/null +++ b/app/assets/images/emoji/shaved_ice.png diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png Binary files differnew file mode 100644 index 00000000000..102b8a52b28 --- /dev/null +++ b/app/assets/images/emoji/sheep.png diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png Binary files differnew file mode 100644 index 00000000000..55721629f62 --- /dev/null +++ b/app/assets/images/emoji/shell.png diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png Binary files differnew file mode 100644 index 00000000000..610bf033ce0 --- /dev/null +++ b/app/assets/images/emoji/shield.png diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png Binary files differnew file mode 100644 index 00000000000..5a344975bf3 --- /dev/null +++ b/app/assets/images/emoji/shinto_shrine.png diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png Binary files differnew file mode 100644 index 00000000000..62d54f7d6c9 --- /dev/null +++ b/app/assets/images/emoji/ship.png diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png Binary files differnew file mode 100644 index 00000000000..af08dec8b59 --- /dev/null +++ b/app/assets/images/emoji/shirt.png diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png Binary files differnew file mode 100644 index 00000000000..99f2a2b13ac --- /dev/null +++ b/app/assets/images/emoji/shopping_bags.png diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png Binary files differnew file mode 100644 index 00000000000..1086fe6e456 --- /dev/null +++ b/app/assets/images/emoji/shopping_cart.png diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png Binary files differnew file mode 100644 index 00000000000..156776a2e52 --- /dev/null +++ b/app/assets/images/emoji/shower.png diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png Binary files differnew file mode 100644 index 00000000000..49eff28a71e --- /dev/null +++ b/app/assets/images/emoji/shrimp.png diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png Binary files differnew file mode 100644 index 00000000000..76e63bfac77 --- /dev/null +++ b/app/assets/images/emoji/shrug.png diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png Binary files differnew file mode 100644 index 00000000000..1c895e64468 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone1.png diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png Binary files differnew file mode 100644 index 00000000000..4e3ca8f8bac --- /dev/null +++ b/app/assets/images/emoji/shrug_tone2.png diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png Binary files differnew file mode 100644 index 00000000000..d1b16a19bb5 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone3.png diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png Binary files differnew file mode 100644 index 00000000000..5fbef3f2255 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone4.png diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png Binary files differnew file mode 100644 index 00000000000..4af2e28bc5c --- /dev/null +++ b/app/assets/images/emoji/shrug_tone5.png diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png Binary files differnew file mode 100644 index 00000000000..ee2b5a4b519 --- /dev/null +++ b/app/assets/images/emoji/signal_strength.png diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png Binary files differnew file mode 100644 index 00000000000..371b3acef2c --- /dev/null +++ b/app/assets/images/emoji/six.png diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png Binary files differnew file mode 100644 index 00000000000..2eb1707458b --- /dev/null +++ b/app/assets/images/emoji/six_pointed_star.png diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png Binary files differnew file mode 100644 index 00000000000..4a2d2c12306 --- /dev/null +++ b/app/assets/images/emoji/ski.png diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png Binary files differnew file mode 100644 index 00000000000..2eb3bdce2af --- /dev/null +++ b/app/assets/images/emoji/skier.png diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png Binary files differnew file mode 100644 index 00000000000..26abb17296a --- /dev/null +++ b/app/assets/images/emoji/skull.png diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png Binary files differnew file mode 100644 index 00000000000..b459df9227a --- /dev/null +++ b/app/assets/images/emoji/skull_crossbones.png diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png Binary files differnew file mode 100644 index 00000000000..9ecf600d6d8 --- /dev/null +++ b/app/assets/images/emoji/sleeping.png diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png Binary files differnew file mode 100644 index 00000000000..c739e7fb69b --- /dev/null +++ b/app/assets/images/emoji/sleeping_accommodation.png diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png Binary files differnew file mode 100644 index 00000000000..836b4107717 --- /dev/null +++ b/app/assets/images/emoji/sleepy.png diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png Binary files differnew file mode 100644 index 00000000000..b2f1d983d36 --- /dev/null +++ b/app/assets/images/emoji/slight_frown.png diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png Binary files differnew file mode 100644 index 00000000000..ddd7d65dd3d --- /dev/null +++ b/app/assets/images/emoji/slight_smile.png diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png Binary files differnew file mode 100644 index 00000000000..ee71b6c268c --- /dev/null +++ b/app/assets/images/emoji/slot_machine.png diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png Binary files differnew file mode 100644 index 00000000000..b86b5bc4db3 --- /dev/null +++ b/app/assets/images/emoji/small_blue_diamond.png diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png Binary files differnew file mode 100644 index 00000000000..e1c6ed9b2f8 --- /dev/null +++ b/app/assets/images/emoji/small_orange_diamond.png diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png Binary files differnew file mode 100644 index 00000000000..785887c195a --- /dev/null +++ b/app/assets/images/emoji/small_red_triangle.png diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png Binary files differnew file mode 100644 index 00000000000..a83beff1914 --- /dev/null +++ b/app/assets/images/emoji/small_red_triangle_down.png diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png Binary files differnew file mode 100644 index 00000000000..aa47ffe978c --- /dev/null +++ b/app/assets/images/emoji/smile.png diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png Binary files differnew file mode 100644 index 00000000000..6f25f11dd3a --- /dev/null +++ b/app/assets/images/emoji/smile_cat.png diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png Binary files differnew file mode 100644 index 00000000000..30957a65968 --- /dev/null +++ b/app/assets/images/emoji/smiley.png diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png Binary files differnew file mode 100644 index 00000000000..163b57a3427 --- /dev/null +++ b/app/assets/images/emoji/smiley_cat.png diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png Binary files differnew file mode 100644 index 00000000000..cc2c5f1ec72 --- /dev/null +++ b/app/assets/images/emoji/smiling_imp.png diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png Binary files differnew file mode 100644 index 00000000000..87852109988 --- /dev/null +++ b/app/assets/images/emoji/smirk.png diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png Binary files differnew file mode 100644 index 00000000000..9ac5954c199 --- /dev/null +++ b/app/assets/images/emoji/smirk_cat.png diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png Binary files differnew file mode 100644 index 00000000000..910f648c8f9 --- /dev/null +++ b/app/assets/images/emoji/smoking.png diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png Binary files differnew file mode 100644 index 00000000000..f4ea071e2d3 --- /dev/null +++ b/app/assets/images/emoji/snail.png diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png Binary files differnew file mode 100644 index 00000000000..d0278a28d8c --- /dev/null +++ b/app/assets/images/emoji/snake.png diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png Binary files differnew file mode 100644 index 00000000000..ccf07d4b64d --- /dev/null +++ b/app/assets/images/emoji/sneezing_face.png diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png Binary files differnew file mode 100644 index 00000000000..6361c0f2c9d --- /dev/null +++ b/app/assets/images/emoji/snowboarder.png diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png Binary files differnew file mode 100644 index 00000000000..db319a77ec6 --- /dev/null +++ b/app/assets/images/emoji/snowflake.png diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png Binary files differnew file mode 100644 index 00000000000..20c177c2aff --- /dev/null +++ b/app/assets/images/emoji/snowman.png diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png Binary files differnew file mode 100644 index 00000000000..896f28502af --- /dev/null +++ b/app/assets/images/emoji/snowman2.png diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png Binary files differnew file mode 100644 index 00000000000..52e3517a1ee --- /dev/null +++ b/app/assets/images/emoji/sob.png diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png Binary files differnew file mode 100644 index 00000000000..28cfa218d6d --- /dev/null +++ b/app/assets/images/emoji/soccer.png diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png Binary files differnew file mode 100644 index 00000000000..8cdfd86690d --- /dev/null +++ b/app/assets/images/emoji/soon.png diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png Binary files differnew file mode 100644 index 00000000000..d7d8c9953e4 --- /dev/null +++ b/app/assets/images/emoji/sos.png diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png Binary files differnew file mode 100644 index 00000000000..e75ddca53ba --- /dev/null +++ b/app/assets/images/emoji/sound.png diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png Binary files differnew file mode 100644 index 00000000000..2e73f5f32e5 --- /dev/null +++ b/app/assets/images/emoji/space_invader.png diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png Binary files differnew file mode 100644 index 00000000000..f822f184cb0 --- /dev/null +++ b/app/assets/images/emoji/spades.png diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png Binary files differnew file mode 100644 index 00000000000..89c24a321f1 --- /dev/null +++ b/app/assets/images/emoji/spaghetti.png diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png Binary files differnew file mode 100644 index 00000000000..6aa7b6ec9cf --- /dev/null +++ b/app/assets/images/emoji/sparkle.png diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png Binary files differnew file mode 100644 index 00000000000..30339cd6e09 --- /dev/null +++ b/app/assets/images/emoji/sparkler.png diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png Binary files differnew file mode 100644 index 00000000000..169bc10b023 --- /dev/null +++ b/app/assets/images/emoji/sparkles.png diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png Binary files differnew file mode 100644 index 00000000000..6709269454e --- /dev/null +++ b/app/assets/images/emoji/sparkling_heart.png diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png Binary files differnew file mode 100644 index 00000000000..9d9e07c974b --- /dev/null +++ b/app/assets/images/emoji/speak_no_evil.png diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png Binary files differnew file mode 100644 index 00000000000..7bcffb8fc43 --- /dev/null +++ b/app/assets/images/emoji/speaker.png diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png Binary files differnew file mode 100644 index 00000000000..2df93aaae09 --- /dev/null +++ b/app/assets/images/emoji/speaking_head.png diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png Binary files differnew file mode 100644 index 00000000000..a34ef741733 --- /dev/null +++ b/app/assets/images/emoji/speech_balloon.png diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png Binary files differnew file mode 100644 index 00000000000..74059d12de1 --- /dev/null +++ b/app/assets/images/emoji/speedboat.png diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png Binary files differnew file mode 100644 index 00000000000..3849fa90b94 --- /dev/null +++ b/app/assets/images/emoji/spider.png diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png Binary files differnew file mode 100644 index 00000000000..ba448ee7fba --- /dev/null +++ b/app/assets/images/emoji/spider_web.png diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png Binary files differnew file mode 100644 index 00000000000..3c4da766aee --- /dev/null +++ b/app/assets/images/emoji/spoon.png diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png Binary files differnew file mode 100644 index 00000000000..a729e9584d6 --- /dev/null +++ b/app/assets/images/emoji/spy.png diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png Binary files differnew file mode 100644 index 00000000000..2d1c022caee --- /dev/null +++ b/app/assets/images/emoji/spy_tone1.png diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png Binary files differnew file mode 100644 index 00000000000..548b9c26f5d --- /dev/null +++ b/app/assets/images/emoji/spy_tone2.png diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png Binary files differnew file mode 100644 index 00000000000..b023f4b18e1 --- /dev/null +++ b/app/assets/images/emoji/spy_tone3.png diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png Binary files differnew file mode 100644 index 00000000000..d8300af492d --- /dev/null +++ b/app/assets/images/emoji/spy_tone4.png diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png Binary files differnew file mode 100644 index 00000000000..ca1462595fa --- /dev/null +++ b/app/assets/images/emoji/spy_tone5.png diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png Binary files differnew file mode 100644 index 00000000000..d2af223f0cb --- /dev/null +++ b/app/assets/images/emoji/squid.png diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png Binary files differnew file mode 100644 index 00000000000..00cd6db5e29 --- /dev/null +++ b/app/assets/images/emoji/stadium.png diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png Binary files differnew file mode 100644 index 00000000000..c930947076e --- /dev/null +++ b/app/assets/images/emoji/star.png diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png Binary files differnew file mode 100644 index 00000000000..2f5cba592db --- /dev/null +++ b/app/assets/images/emoji/star2.png diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png Binary files differnew file mode 100644 index 00000000000..e182636457d --- /dev/null +++ b/app/assets/images/emoji/star_and_crescent.png diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png Binary files differnew file mode 100644 index 00000000000..fc59d0dde24 --- /dev/null +++ b/app/assets/images/emoji/star_of_david.png diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png Binary files differnew file mode 100644 index 00000000000..aa45384d1c6 --- /dev/null +++ b/app/assets/images/emoji/stars.png diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png Binary files differnew file mode 100644 index 00000000000..5c26fee529c --- /dev/null +++ b/app/assets/images/emoji/station.png diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png Binary files differnew file mode 100644 index 00000000000..05df8289b59 --- /dev/null +++ b/app/assets/images/emoji/statue_of_liberty.png diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png Binary files differnew file mode 100644 index 00000000000..9ac0d999c4c --- /dev/null +++ b/app/assets/images/emoji/steam_locomotive.png diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png Binary files differnew file mode 100644 index 00000000000..6b3f010c17a --- /dev/null +++ b/app/assets/images/emoji/stew.png diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png Binary files differnew file mode 100644 index 00000000000..cfa99988ac2 --- /dev/null +++ b/app/assets/images/emoji/stop_button.png diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png Binary files differnew file mode 100644 index 00000000000..8fae1c9a898 --- /dev/null +++ b/app/assets/images/emoji/stopwatch.png diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png Binary files differnew file mode 100644 index 00000000000..1017b7433a1 --- /dev/null +++ b/app/assets/images/emoji/straight_ruler.png diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png Binary files differnew file mode 100644 index 00000000000..7bb86f0b29c --- /dev/null +++ b/app/assets/images/emoji/strawberry.png diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png Binary files differnew file mode 100644 index 00000000000..25757341f96 --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue.png diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png Binary files differnew file mode 100644 index 00000000000..5c0401e9b1d --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png Binary files differnew file mode 100644 index 00000000000..4817eaa3dc6 --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png Binary files differnew file mode 100644 index 00000000000..a2e10df40a5 --- /dev/null +++ b/app/assets/images/emoji/stuffed_flatbread.png diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png Binary files differnew file mode 100644 index 00000000000..14a4ea971db --- /dev/null +++ b/app/assets/images/emoji/sun_with_face.png diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png Binary files differnew file mode 100644 index 00000000000..08cc07761ea --- /dev/null +++ b/app/assets/images/emoji/sunflower.png diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png Binary files differnew file mode 100644 index 00000000000..20011735110 --- /dev/null +++ b/app/assets/images/emoji/sunglasses.png diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png Binary files differnew file mode 100644 index 00000000000..fd521ae31a7 --- /dev/null +++ b/app/assets/images/emoji/sunny.png diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png Binary files differnew file mode 100644 index 00000000000..4ad36003c20 --- /dev/null +++ b/app/assets/images/emoji/sunrise.png diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png Binary files differnew file mode 100644 index 00000000000..2b99307344d --- /dev/null +++ b/app/assets/images/emoji/sunrise_over_mountains.png diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png Binary files differnew file mode 100644 index 00000000000..3ab017adf4b --- /dev/null +++ b/app/assets/images/emoji/surfer.png diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png Binary files differnew file mode 100644 index 00000000000..b5faaa524cc --- /dev/null +++ b/app/assets/images/emoji/surfer_tone1.png diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png Binary files differnew file mode 100644 index 00000000000..6d92e412ff1 --- /dev/null +++ b/app/assets/images/emoji/surfer_tone2.png diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png Binary files differnew file mode 100644 index 00000000000..f05ef59496e --- /dev/null +++ b/app/assets/images/emoji/surfer_tone3.png diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png Binary files differnew file mode 100644 index 00000000000..35e143d19dc --- /dev/null +++ b/app/assets/images/emoji/surfer_tone4.png diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png Binary files differnew file mode 100644 index 00000000000..38917658eac --- /dev/null +++ b/app/assets/images/emoji/surfer_tone5.png diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png Binary files differnew file mode 100644 index 00000000000..f171fd2f7a1 --- /dev/null +++ b/app/assets/images/emoji/sushi.png diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png Binary files differnew file mode 100644 index 00000000000..a59d5f48c24 --- /dev/null +++ b/app/assets/images/emoji/suspension_railway.png diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png Binary files differnew file mode 100644 index 00000000000..f0dae7b7893 --- /dev/null +++ b/app/assets/images/emoji/sweat.png diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png Binary files differnew file mode 100644 index 00000000000..4106117ebc8 --- /dev/null +++ b/app/assets/images/emoji/sweat_drops.png diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png Binary files differnew file mode 100644 index 00000000000..cb18d9c899b --- /dev/null +++ b/app/assets/images/emoji/sweat_smile.png diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png Binary files differnew file mode 100644 index 00000000000..92a425f2e20 --- /dev/null +++ b/app/assets/images/emoji/sweet_potato.png diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png Binary files differnew file mode 100644 index 00000000000..55b4d72f9a7 --- /dev/null +++ b/app/assets/images/emoji/swimmer.png diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png Binary files differnew file mode 100644 index 00000000000..38441c9ca9a --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone1.png diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png Binary files differnew file mode 100644 index 00000000000..b0d43112444 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone2.png diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png Binary files differnew file mode 100644 index 00000000000..211e77e2aa0 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone3.png diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png Binary files differnew file mode 100644 index 00000000000..f34c34db9d2 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone4.png diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png Binary files differnew file mode 100644 index 00000000000..3e9231ff868 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone5.png diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png Binary files differnew file mode 100644 index 00000000000..ac2fc1f358f --- /dev/null +++ b/app/assets/images/emoji/symbols.png diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png Binary files differnew file mode 100644 index 00000000000..ee347904c80 --- /dev/null +++ b/app/assets/images/emoji/synagogue.png diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png Binary files differnew file mode 100644 index 00000000000..71c1a9528d5 --- /dev/null +++ b/app/assets/images/emoji/syringe.png diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png Binary files differnew file mode 100644 index 00000000000..10e847a4619 --- /dev/null +++ b/app/assets/images/emoji/taco.png diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png Binary files differnew file mode 100644 index 00000000000..0244d60f269 --- /dev/null +++ b/app/assets/images/emoji/tada.png diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png Binary files differnew file mode 100644 index 00000000000..46fcb3a1aac --- /dev/null +++ b/app/assets/images/emoji/tanabata_tree.png diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png Binary files differnew file mode 100644 index 00000000000..ab14e5378db --- /dev/null +++ b/app/assets/images/emoji/tangerine.png diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png Binary files differnew file mode 100644 index 00000000000..b2a370df42b --- /dev/null +++ b/app/assets/images/emoji/taurus.png diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png Binary files differnew file mode 100644 index 00000000000..55f4cc84797 --- /dev/null +++ b/app/assets/images/emoji/taxi.png diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png Binary files differnew file mode 100644 index 00000000000..b53b98f0c45 --- /dev/null +++ b/app/assets/images/emoji/tea.png diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png Binary files differnew file mode 100644 index 00000000000..a1e69f566bc --- /dev/null +++ b/app/assets/images/emoji/telephone.png diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png Binary files differnew file mode 100644 index 00000000000..69388316c35 --- /dev/null +++ b/app/assets/images/emoji/telephone_receiver.png diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png Binary files differnew file mode 100644 index 00000000000..d63154614b5 --- /dev/null +++ b/app/assets/images/emoji/telescope.png diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png Binary files differnew file mode 100644 index 00000000000..782d4004962 --- /dev/null +++ b/app/assets/images/emoji/ten.png diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png Binary files differnew file mode 100644 index 00000000000..7e68ba8f301 --- /dev/null +++ b/app/assets/images/emoji/tennis.png diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png Binary files differnew file mode 100644 index 00000000000..3fddcfc56eb --- /dev/null +++ b/app/assets/images/emoji/tent.png diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png Binary files differnew file mode 100644 index 00000000000..b1147392426 --- /dev/null +++ b/app/assets/images/emoji/thermometer.png diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png Binary files differnew file mode 100644 index 00000000000..8fc57387563 --- /dev/null +++ b/app/assets/images/emoji/thermometer_face.png diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png Binary files differnew file mode 100644 index 00000000000..c18f6fd14ad --- /dev/null +++ b/app/assets/images/emoji/thinking.png diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png Binary files differnew file mode 100644 index 00000000000..636e04a5950 --- /dev/null +++ b/app/assets/images/emoji/third_place.png diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png Binary files differnew file mode 100644 index 00000000000..72fe8fa7022 --- /dev/null +++ b/app/assets/images/emoji/thought_balloon.png diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png Binary files differnew file mode 100644 index 00000000000..dbaa6183e72 --- /dev/null +++ b/app/assets/images/emoji/three.png diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png Binary files differnew file mode 100644 index 00000000000..b63da2f20a8 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown.png diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png Binary files differnew file mode 100644 index 00000000000..a1631af8e92 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone1.png diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png Binary files differnew file mode 100644 index 00000000000..85fff82d595 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone2.png diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png Binary files differnew file mode 100644 index 00000000000..eeba3be80fd --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone3.png diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png Binary files differnew file mode 100644 index 00000000000..1addafdaed0 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone4.png diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png Binary files differnew file mode 100644 index 00000000000..37ec07b5721 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone5.png diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png Binary files differnew file mode 100644 index 00000000000..f9e6f13a34f --- /dev/null +++ b/app/assets/images/emoji/thumbsup.png diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png Binary files differnew file mode 100644 index 00000000000..39684cd5cc7 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone1.png diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png Binary files differnew file mode 100644 index 00000000000..a9b59723573 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone2.png diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png Binary files differnew file mode 100644 index 00000000000..c5e29167015 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone3.png diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png Binary files differnew file mode 100644 index 00000000000..5bf4857a884 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone4.png diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png Binary files differnew file mode 100644 index 00000000000..d829f787c61 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone5.png diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png Binary files differnew file mode 100644 index 00000000000..31a26a1b6ee --- /dev/null +++ b/app/assets/images/emoji/thunder_cloud_rain.png diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png Binary files differnew file mode 100644 index 00000000000..605936bb6b3 --- /dev/null +++ b/app/assets/images/emoji/ticket.png diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png Binary files differnew file mode 100644 index 00000000000..e510f4a7a50 --- /dev/null +++ b/app/assets/images/emoji/tickets.png diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png Binary files differnew file mode 100644 index 00000000000..a4d3ef086d4 --- /dev/null +++ b/app/assets/images/emoji/tiger.png diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png Binary files differnew file mode 100644 index 00000000000..871a8b74d56 --- /dev/null +++ b/app/assets/images/emoji/tiger2.png diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png Binary files differnew file mode 100644 index 00000000000..8a3be574c24 --- /dev/null +++ b/app/assets/images/emoji/timer.png diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png Binary files differnew file mode 100644 index 00000000000..4e01eff5b23 --- /dev/null +++ b/app/assets/images/emoji/tired_face.png diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png Binary files differnew file mode 100644 index 00000000000..7a0c44a2c2b --- /dev/null +++ b/app/assets/images/emoji/tm.png diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png Binary files differnew file mode 100644 index 00000000000..1392f761835 --- /dev/null +++ b/app/assets/images/emoji/toilet.png diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png Binary files differnew file mode 100644 index 00000000000..37df7fc65b1 --- /dev/null +++ b/app/assets/images/emoji/tokyo_tower.png diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png Binary files differnew file mode 100644 index 00000000000..497da8f6b22 --- /dev/null +++ b/app/assets/images/emoji/tomato.png diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png Binary files differnew file mode 100644 index 00000000000..c395f3d0d68 --- /dev/null +++ b/app/assets/images/emoji/tone1.png diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png Binary files differnew file mode 100644 index 00000000000..080847431c1 --- /dev/null +++ b/app/assets/images/emoji/tone2.png diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png Binary files differnew file mode 100644 index 00000000000..482dd403475 --- /dev/null +++ b/app/assets/images/emoji/tone3.png diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png Binary files differnew file mode 100644 index 00000000000..5cae8bb20b0 --- /dev/null +++ b/app/assets/images/emoji/tone4.png diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png Binary files differnew file mode 100644 index 00000000000..49d1a8c3a64 --- /dev/null +++ b/app/assets/images/emoji/tone5.png diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png Binary files differnew file mode 100644 index 00000000000..70ce9c1225f --- /dev/null +++ b/app/assets/images/emoji/tongue.png diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png Binary files differnew file mode 100644 index 00000000000..3c6049273a9 --- /dev/null +++ b/app/assets/images/emoji/tools.png diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png Binary files differnew file mode 100644 index 00000000000..49dea8c08b5 --- /dev/null +++ b/app/assets/images/emoji/top.png diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png Binary files differnew file mode 100644 index 00000000000..131b657b109 --- /dev/null +++ b/app/assets/images/emoji/tophat.png diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png Binary files differnew file mode 100644 index 00000000000..f8880d33bab --- /dev/null +++ b/app/assets/images/emoji/track_next.png diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png Binary files differnew file mode 100644 index 00000000000..1ffd0566cfc --- /dev/null +++ b/app/assets/images/emoji/track_previous.png diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png Binary files differnew file mode 100644 index 00000000000..3bea84ad7ce --- /dev/null +++ b/app/assets/images/emoji/trackball.png diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png Binary files differnew file mode 100644 index 00000000000..c1bf8cae44f --- /dev/null +++ b/app/assets/images/emoji/tractor.png diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png Binary files differnew file mode 100644 index 00000000000..6b312285b00 --- /dev/null +++ b/app/assets/images/emoji/traffic_light.png diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png Binary files differnew file mode 100644 index 00000000000..3c80321f7e8 --- /dev/null +++ b/app/assets/images/emoji/train.png diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png Binary files differnew file mode 100644 index 00000000000..367c7bc5d39 --- /dev/null +++ b/app/assets/images/emoji/train2.png diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png Binary files differnew file mode 100644 index 00000000000..b6f0e69038f --- /dev/null +++ b/app/assets/images/emoji/tram.png diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png Binary files differnew file mode 100644 index 00000000000..c12d8b06886 --- /dev/null +++ b/app/assets/images/emoji/triangular_flag_on_post.png diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png Binary files differnew file mode 100644 index 00000000000..77dee9ee843 --- /dev/null +++ b/app/assets/images/emoji/triangular_ruler.png diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png Binary files differnew file mode 100644 index 00000000000..777a1dad121 --- /dev/null +++ b/app/assets/images/emoji/trident.png diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png Binary files differnew file mode 100644 index 00000000000..0be7a501969 --- /dev/null +++ b/app/assets/images/emoji/triumph.png diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png Binary files differnew file mode 100644 index 00000000000..139a9931b52 --- /dev/null +++ b/app/assets/images/emoji/trolleybus.png diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png Binary files differnew file mode 100644 index 00000000000..ac2895c1896 --- /dev/null +++ b/app/assets/images/emoji/trophy.png diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png Binary files differnew file mode 100644 index 00000000000..cd714f81b36 --- /dev/null +++ b/app/assets/images/emoji/tropical_drink.png diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png Binary files differnew file mode 100644 index 00000000000..252105235a6 --- /dev/null +++ b/app/assets/images/emoji/tropical_fish.png diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png Binary files differnew file mode 100644 index 00000000000..130de047f8b --- /dev/null +++ b/app/assets/images/emoji/truck.png diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png Binary files differnew file mode 100644 index 00000000000..864ccbcd04a --- /dev/null +++ b/app/assets/images/emoji/trumpet.png diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png Binary files differnew file mode 100644 index 00000000000..f799d75c182 --- /dev/null +++ b/app/assets/images/emoji/tulip.png diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png Binary files differnew file mode 100644 index 00000000000..7bf09229879 --- /dev/null +++ b/app/assets/images/emoji/tumbler_glass.png diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png Binary files differnew file mode 100644 index 00000000000..344af94c9ec --- /dev/null +++ b/app/assets/images/emoji/turkey.png diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png Binary files differnew file mode 100644 index 00000000000..c22f7519fe8 --- /dev/null +++ b/app/assets/images/emoji/turtle.png diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png Binary files differnew file mode 100644 index 00000000000..999f1fb5c6d --- /dev/null +++ b/app/assets/images/emoji/tv.png diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png Binary files differnew file mode 100644 index 00000000000..5904badde65 --- /dev/null +++ b/app/assets/images/emoji/twisted_rightwards_arrows.png diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png Binary files differnew file mode 100644 index 00000000000..927339c9bff --- /dev/null +++ b/app/assets/images/emoji/two.png diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png Binary files differnew file mode 100644 index 00000000000..4d8c3386042 --- /dev/null +++ b/app/assets/images/emoji/two_hearts.png diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png Binary files differnew file mode 100644 index 00000000000..a511fda822a --- /dev/null +++ b/app/assets/images/emoji/two_men_holding_hands.png diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png Binary files differnew file mode 100644 index 00000000000..b077cd3e40f --- /dev/null +++ b/app/assets/images/emoji/two_women_holding_hands.png diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png Binary files differnew file mode 100644 index 00000000000..c4f837fe684 --- /dev/null +++ b/app/assets/images/emoji/u5272.png diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png Binary files differnew file mode 100644 index 00000000000..8375ad9d9af --- /dev/null +++ b/app/assets/images/emoji/u5408.png diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png Binary files differnew file mode 100644 index 00000000000..d21cb30eaf3 --- /dev/null +++ b/app/assets/images/emoji/u55b6.png diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png Binary files differnew file mode 100644 index 00000000000..078e23e4ff3 --- /dev/null +++ b/app/assets/images/emoji/u6307.png diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png Binary files differnew file mode 100644 index 00000000000..c41bd36a26a --- /dev/null +++ b/app/assets/images/emoji/u6708.png diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png Binary files differnew file mode 100644 index 00000000000..a4510de41c0 --- /dev/null +++ b/app/assets/images/emoji/u6709.png diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png Binary files differnew file mode 100644 index 00000000000..f9dea8b8833 --- /dev/null +++ b/app/assets/images/emoji/u6e80.png diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png Binary files differnew file mode 100644 index 00000000000..d3a19b420de --- /dev/null +++ b/app/assets/images/emoji/u7121.png diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png Binary files differnew file mode 100644 index 00000000000..6b7af0ee222 --- /dev/null +++ b/app/assets/images/emoji/u7533.png diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png Binary files differnew file mode 100644 index 00000000000..4c704e03433 --- /dev/null +++ b/app/assets/images/emoji/u7981.png diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png Binary files differnew file mode 100644 index 00000000000..47966c1ea93 --- /dev/null +++ b/app/assets/images/emoji/u7a7a.png diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png Binary files differnew file mode 100644 index 00000000000..5b35b7ff6a4 --- /dev/null +++ b/app/assets/images/emoji/umbrella.png diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png Binary files differnew file mode 100644 index 00000000000..97fe859e74f --- /dev/null +++ b/app/assets/images/emoji/umbrella2.png diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png Binary files differnew file mode 100644 index 00000000000..25e3677f2eb --- /dev/null +++ b/app/assets/images/emoji/unamused.png diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png Binary files differnew file mode 100644 index 00000000000..6dfe6da51e2 --- /dev/null +++ b/app/assets/images/emoji/underage.png diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png Binary files differnew file mode 100644 index 00000000000..05a97969f7e --- /dev/null +++ b/app/assets/images/emoji/unicorn.png diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png Binary files differnew file mode 100644 index 00000000000..4a74a693911 --- /dev/null +++ b/app/assets/images/emoji/unlock.png diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png Binary files differnew file mode 100644 index 00000000000..0d42142ba04 --- /dev/null +++ b/app/assets/images/emoji/up.png diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png Binary files differnew file mode 100644 index 00000000000..128f31c9828 --- /dev/null +++ b/app/assets/images/emoji/upside_down.png diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png Binary files differnew file mode 100644 index 00000000000..6b5b3503438 --- /dev/null +++ b/app/assets/images/emoji/urn.png diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png Binary files differnew file mode 100644 index 00000000000..70c5516ffee --- /dev/null +++ b/app/assets/images/emoji/v.png diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png Binary files differnew file mode 100644 index 00000000000..6ac54a745f4 --- /dev/null +++ b/app/assets/images/emoji/v_tone1.png diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png Binary files differnew file mode 100644 index 00000000000..6dd9669866d --- /dev/null +++ b/app/assets/images/emoji/v_tone2.png diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png Binary files differnew file mode 100644 index 00000000000..a615e53f02f --- /dev/null +++ b/app/assets/images/emoji/v_tone3.png diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png Binary files differnew file mode 100644 index 00000000000..33a34bd5a78 --- /dev/null +++ b/app/assets/images/emoji/v_tone4.png diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png Binary files differnew file mode 100644 index 00000000000..45ad14b6c9c --- /dev/null +++ b/app/assets/images/emoji/v_tone5.png diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png Binary files differnew file mode 100644 index 00000000000..8085973eecf --- /dev/null +++ b/app/assets/images/emoji/vertical_traffic_light.png diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png Binary files differnew file mode 100644 index 00000000000..b9eb78ecd92 --- /dev/null +++ b/app/assets/images/emoji/vhs.png diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png Binary files differnew file mode 100644 index 00000000000..cc46510e48e --- /dev/null +++ b/app/assets/images/emoji/vibration_mode.png diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png Binary files differnew file mode 100644 index 00000000000..85b300d425c --- /dev/null +++ b/app/assets/images/emoji/video_camera.png diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png Binary files differnew file mode 100644 index 00000000000..316a9106a55 --- /dev/null +++ b/app/assets/images/emoji/video_game.png diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png Binary files differnew file mode 100644 index 00000000000..e1e76cce242 --- /dev/null +++ b/app/assets/images/emoji/violin.png diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png Binary files differnew file mode 100644 index 00000000000..a6b56c2cb5e --- /dev/null +++ b/app/assets/images/emoji/virgo.png diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png Binary files differnew file mode 100644 index 00000000000..931d569294c --- /dev/null +++ b/app/assets/images/emoji/volcano.png diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png Binary files differnew file mode 100644 index 00000000000..7a0e49d4b07 --- /dev/null +++ b/app/assets/images/emoji/volleyball.png diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png Binary files differnew file mode 100644 index 00000000000..e1180f4a464 --- /dev/null +++ b/app/assets/images/emoji/vs.png diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png Binary files differnew file mode 100644 index 00000000000..54728bcaf5c --- /dev/null +++ b/app/assets/images/emoji/vulcan.png diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png Binary files differnew file mode 100644 index 00000000000..8aff5d8fa16 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone1.png diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png Binary files differnew file mode 100644 index 00000000000..82b7ad519b4 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone2.png diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png Binary files differnew file mode 100644 index 00000000000..d1400e1dd28 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone3.png diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png Binary files differnew file mode 100644 index 00000000000..47e2b280148 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone4.png diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png Binary files differnew file mode 100644 index 00000000000..60b5c6077be --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone5.png diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png Binary files differnew file mode 100644 index 00000000000..06dc169a3fd --- /dev/null +++ b/app/assets/images/emoji/walking.png diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png Binary files differnew file mode 100644 index 00000000000..4e391b45a0b --- /dev/null +++ b/app/assets/images/emoji/walking_tone1.png diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png Binary files differnew file mode 100644 index 00000000000..31f94a1bce1 --- /dev/null +++ b/app/assets/images/emoji/walking_tone2.png diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png Binary files differnew file mode 100644 index 00000000000..f7ed8e39c2e --- /dev/null +++ b/app/assets/images/emoji/walking_tone3.png diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png Binary files differnew file mode 100644 index 00000000000..e58dc04c7b2 --- /dev/null +++ b/app/assets/images/emoji/walking_tone4.png diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png Binary files differnew file mode 100644 index 00000000000..ba4e1b58fcb --- /dev/null +++ b/app/assets/images/emoji/walking_tone5.png diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png Binary files differnew file mode 100644 index 00000000000..cf68706b871 --- /dev/null +++ b/app/assets/images/emoji/waning_crescent_moon.png diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png Binary files differnew file mode 100644 index 00000000000..24e16266119 --- /dev/null +++ b/app/assets/images/emoji/waning_gibbous_moon.png diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png Binary files differnew file mode 100644 index 00000000000..35691c2ed97 --- /dev/null +++ b/app/assets/images/emoji/warning.png diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png Binary files differnew file mode 100644 index 00000000000..2b3c484b498 --- /dev/null +++ b/app/assets/images/emoji/wastebasket.png diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png Binary files differnew file mode 100644 index 00000000000..64819bc6e21 --- /dev/null +++ b/app/assets/images/emoji/watch.png diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png Binary files differnew file mode 100644 index 00000000000..80446615caf --- /dev/null +++ b/app/assets/images/emoji/water_buffalo.png diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png Binary files differnew file mode 100644 index 00000000000..cb44576780d --- /dev/null +++ b/app/assets/images/emoji/water_polo.png diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png Binary files differnew file mode 100644 index 00000000000..bed1a908d6a --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone1.png diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png Binary files differnew file mode 100644 index 00000000000..ec5a43b4d4a --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone2.png diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png Binary files differnew file mode 100644 index 00000000000..b081a4a5a96 --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone3.png diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png Binary files differnew file mode 100644 index 00000000000..82cfbc3b0c7 --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone4.png diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png Binary files differnew file mode 100644 index 00000000000..bd3366eb06c --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone5.png diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png Binary files differnew file mode 100644 index 00000000000..0761488b4c9 --- /dev/null +++ b/app/assets/images/emoji/watermelon.png diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png Binary files differnew file mode 100644 index 00000000000..e0cd79b45f5 --- /dev/null +++ b/app/assets/images/emoji/wave.png diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png Binary files differnew file mode 100644 index 00000000000..6b2b34b106e --- /dev/null +++ b/app/assets/images/emoji/wave_tone1.png diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png Binary files differnew file mode 100644 index 00000000000..b857119732e --- /dev/null +++ b/app/assets/images/emoji/wave_tone2.png diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png Binary files differnew file mode 100644 index 00000000000..6283b670f43 --- /dev/null +++ b/app/assets/images/emoji/wave_tone3.png diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png Binary files differnew file mode 100644 index 00000000000..fe6b2baa747 --- /dev/null +++ b/app/assets/images/emoji/wave_tone4.png diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png Binary files differnew file mode 100644 index 00000000000..4bd168ebb78 --- /dev/null +++ b/app/assets/images/emoji/wave_tone5.png diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png Binary files differnew file mode 100644 index 00000000000..001c8d6e47d --- /dev/null +++ b/app/assets/images/emoji/wavy_dash.png diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png Binary files differnew file mode 100644 index 00000000000..687125173d9 --- /dev/null +++ b/app/assets/images/emoji/waxing_crescent_moon.png diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png Binary files differnew file mode 100644 index 00000000000..3a808156318 --- /dev/null +++ b/app/assets/images/emoji/waxing_gibbous_moon.png diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png Binary files differnew file mode 100644 index 00000000000..aa433e84ba6 --- /dev/null +++ b/app/assets/images/emoji/wc.png diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png Binary files differnew file mode 100644 index 00000000000..98bfbd24a16 --- /dev/null +++ b/app/assets/images/emoji/weary.png diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png Binary files differnew file mode 100644 index 00000000000..d0d8aa0bfae --- /dev/null +++ b/app/assets/images/emoji/wedding.png diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png Binary files differnew file mode 100644 index 00000000000..9f19b44257c --- /dev/null +++ b/app/assets/images/emoji/whale.png diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png Binary files differnew file mode 100644 index 00000000000..0df9d3c73a4 --- /dev/null +++ b/app/assets/images/emoji/whale2.png diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png Binary files differnew file mode 100644 index 00000000000..3666db0016b --- /dev/null +++ b/app/assets/images/emoji/wheel_of_dharma.png diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png Binary files differnew file mode 100644 index 00000000000..4e5b2698eac --- /dev/null +++ b/app/assets/images/emoji/wheelchair.png diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png Binary files differnew file mode 100644 index 00000000000..e55f087e544 --- /dev/null +++ b/app/assets/images/emoji/white_check_mark.png diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png Binary files differnew file mode 100644 index 00000000000..c19e15684dd --- /dev/null +++ b/app/assets/images/emoji/white_circle.png diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png Binary files differnew file mode 100644 index 00000000000..d6af8b60077 --- /dev/null +++ b/app/assets/images/emoji/white_flower.png diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png Binary files differnew file mode 100644 index 00000000000..6f06c1c79de --- /dev/null +++ b/app/assets/images/emoji/white_large_square.png diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png Binary files differnew file mode 100644 index 00000000000..ae874126750 --- /dev/null +++ b/app/assets/images/emoji/white_medium_small_square.png diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png Binary files differnew file mode 100644 index 00000000000..8daacf57059 --- /dev/null +++ b/app/assets/images/emoji/white_medium_square.png diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png Binary files differnew file mode 100644 index 00000000000..d7ebdb0c0ed --- /dev/null +++ b/app/assets/images/emoji/white_small_square.png diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png Binary files differnew file mode 100644 index 00000000000..934b1cedfd2 --- /dev/null +++ b/app/assets/images/emoji/white_square_button.png diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png Binary files differnew file mode 100644 index 00000000000..0a4cc100269 --- /dev/null +++ b/app/assets/images/emoji/white_sun_cloud.png diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png Binary files differnew file mode 100644 index 00000000000..491f9ca4839 --- /dev/null +++ b/app/assets/images/emoji/white_sun_rain_cloud.png diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png Binary files differnew file mode 100644 index 00000000000..cead0bfa521 --- /dev/null +++ b/app/assets/images/emoji/white_sun_small_cloud.png diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png Binary files differnew file mode 100644 index 00000000000..62412b143ae --- /dev/null +++ b/app/assets/images/emoji/wilted_rose.png diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png Binary files differnew file mode 100644 index 00000000000..df81b652eb6 --- /dev/null +++ b/app/assets/images/emoji/wind_blowing_face.png diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png Binary files differnew file mode 100644 index 00000000000..3c9ef3a95f6 --- /dev/null +++ b/app/assets/images/emoji/wind_chime.png diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png Binary files differnew file mode 100644 index 00000000000..3cc98689192 --- /dev/null +++ b/app/assets/images/emoji/wine_glass.png diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png Binary files differnew file mode 100644 index 00000000000..7ea7810a37d --- /dev/null +++ b/app/assets/images/emoji/wink.png diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png Binary files differnew file mode 100644 index 00000000000..ba7220f2de9 --- /dev/null +++ b/app/assets/images/emoji/wolf.png diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png Binary files differnew file mode 100644 index 00000000000..ece440e7a61 --- /dev/null +++ b/app/assets/images/emoji/woman.png diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png Binary files differnew file mode 100644 index 00000000000..ff089b8889b --- /dev/null +++ b/app/assets/images/emoji/woman_tone1.png diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png Binary files differnew file mode 100644 index 00000000000..0719c378016 --- /dev/null +++ b/app/assets/images/emoji/woman_tone2.png diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png Binary files differnew file mode 100644 index 00000000000..5672e2fd52d --- /dev/null +++ b/app/assets/images/emoji/woman_tone3.png diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png Binary files differnew file mode 100644 index 00000000000..5754aab558b --- /dev/null +++ b/app/assets/images/emoji/woman_tone4.png diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png Binary files differnew file mode 100644 index 00000000000..fc252af3a39 --- /dev/null +++ b/app/assets/images/emoji/woman_tone5.png diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png Binary files differnew file mode 100644 index 00000000000..01410dc8107 --- /dev/null +++ b/app/assets/images/emoji/womans_clothes.png diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png Binary files differnew file mode 100644 index 00000000000..b837b6a2e47 --- /dev/null +++ b/app/assets/images/emoji/womans_hat.png diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png Binary files differnew file mode 100644 index 00000000000..d4ecc22e7b3 --- /dev/null +++ b/app/assets/images/emoji/womens.png diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png Binary files differnew file mode 100644 index 00000000000..7074afcf5b7 --- /dev/null +++ b/app/assets/images/emoji/worried.png diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png Binary files differnew file mode 100644 index 00000000000..c16b7439697 --- /dev/null +++ b/app/assets/images/emoji/wrench.png diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png Binary files differnew file mode 100644 index 00000000000..71e67cfad85 --- /dev/null +++ b/app/assets/images/emoji/wrestlers.png diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png Binary files differnew file mode 100644 index 00000000000..379070fd03b --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone1.png diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png Binary files differnew file mode 100644 index 00000000000..6863ea9209d --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone2.png diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png Binary files differnew file mode 100644 index 00000000000..b7e62910127 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone3.png diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png Binary files differnew file mode 100644 index 00000000000..750f9589233 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone4.png diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png Binary files differnew file mode 100644 index 00000000000..36ab9bb3f42 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone5.png diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png Binary files differnew file mode 100644 index 00000000000..85639f8ac40 --- /dev/null +++ b/app/assets/images/emoji/writing_hand.png diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..7923d8ebb17 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone1.png diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..bcb304e15d2 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone2.png diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..fd885fd2d90 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone3.png diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..d065b8c64ab --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone4.png diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..a44b3dd757c --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone5.png diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png Binary files differnew file mode 100644 index 00000000000..9f9ed0f7ad2 --- /dev/null +++ b/app/assets/images/emoji/x.png diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png Binary files differnew file mode 100644 index 00000000000..7901a9d0103 --- /dev/null +++ b/app/assets/images/emoji/yellow_heart.png diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png Binary files differnew file mode 100644 index 00000000000..63ee4799d66 --- /dev/null +++ b/app/assets/images/emoji/yen.png diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png Binary files differnew file mode 100644 index 00000000000..f2900f6338f --- /dev/null +++ b/app/assets/images/emoji/yin_yang.png diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png Binary files differnew file mode 100644 index 00000000000..2df15753ca1 --- /dev/null +++ b/app/assets/images/emoji/yum.png diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png Binary files differnew file mode 100644 index 00000000000..47e68e48e49 --- /dev/null +++ b/app/assets/images/emoji/zap.png diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png Binary files differnew file mode 100644 index 00000000000..13aca83e018 --- /dev/null +++ b/app/assets/images/emoji/zero.png diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png Binary files differnew file mode 100644 index 00000000000..f8ced2502a7 --- /dev/null +++ b/app/assets/images/emoji/zipper_mouth.png diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png Binary files differnew file mode 100644 index 00000000000..9bc72b4469f --- /dev/null +++ b/app/assets/images/emoji/zzz.png diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png Binary files differindex dc9cae1d44c..b0fa9e1139e 100644 --- a/app/assets/images/emoji@2x.png +++ b/app/assets/images/emoji@2x.png diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg new file mode 100644 index 00000000000..c4d8e65122d --- /dev/null +++ b/app/assets/images/icon-merge-request-unmerged.svg @@ -0,0 +1 @@ +<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg> diff --git a/app/assets/images/mailers/gitlab_footer_logo.gif b/app/assets/images/mailers/gitlab_footer_logo.gif Binary files differnew file mode 100644 index 00000000000..3f4ef31947b --- /dev/null +++ b/app/assets/images/mailers/gitlab_footer_logo.gif diff --git a/app/assets/images/mailers/gitlab_header_logo.gif b/app/assets/images/mailers/gitlab_header_logo.gif Binary files differnew file mode 100644 index 00000000000..387628f831c --- /dev/null +++ b/app/assets/images/mailers/gitlab_header_logo.gif diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js new file mode 100644 index 00000000000..346de4ad11e --- /dev/null +++ b/app/assets/javascripts/abuse_reports.js @@ -0,0 +1,37 @@ +const MAX_MESSAGE_LENGTH = 500; +const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; + +class AbuseReports { + constructor() { + $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage); + $(document) + .off('click', MESSAGE_CELL_SELECTOR) + .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation); + } + + truncateLongMessage() { + const $messageCellElement = $(this); + const reportMessage = $messageCellElement.text(); + if (reportMessage.length > MAX_MESSAGE_LENGTH) { + $messageCellElement.data('original-message', reportMessage); + $messageCellElement.data('message-truncated', 'true'); + $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); + } + } + + toggleMessageTruncation() { + const $messageCellElement = $(this); + const originalMessage = $messageCellElement.data('original-message'); + if (!originalMessage) return; + if ($messageCellElement.data('message-truncated') === 'true') { + $messageCellElement.data('message-truncated', 'false'); + $messageCellElement.text(originalMessage); + } else { + $messageCellElement.data('message-truncated', 'true'); + $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); + } + } +} + +window.gl = window.gl || {}; +window.gl.AbuseReports = AbuseReports; diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6 deleted file mode 100644 index 8a260aae1b1..00000000000 --- a/app/assets/javascripts/abuse_reports.js.es6 +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable no-param-reassign */ - -((global) => { - const MAX_MESSAGE_LENGTH = 500; - const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; - - class AbuseReports { - constructor() { - $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage); - $(document) - .off('click', MESSAGE_CELL_SELECTOR) - .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation); - } - - truncateLongMessage() { - const $messageCellElement = $(this); - const reportMessage = $messageCellElement.text(); - if (reportMessage.length > MAX_MESSAGE_LENGTH) { - $messageCellElement.data('original-message', reportMessage); - $messageCellElement.data('message-truncated', 'true'); - $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); - } - } - - toggleMessageTruncation() { - const $messageCellElement = $(this); - const originalMessage = $messageCellElement.data('original-message'); - if (!originalMessage) return; - if ($messageCellElement.data('message-truncated') === 'true') { - $messageCellElement.data('message-truncated', 'false'); - $messageCellElement.text(originalMessage); - } else { - $messageCellElement.data('message-truncated', 'true'); - $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); - } - } - } - - global.AbuseReports = AbuseReports; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js new file mode 100644 index 00000000000..aebda7780e1 --- /dev/null +++ b/app/assets/javascripts/activities.js @@ -0,0 +1,36 @@ +/* eslint-disable no-param-reassign, class-methods-use-this */ +/* global Pager */ +/* global Cookies */ + +class Activities { + constructor() { + Pager.init(20, true, false, this.updateTooltips); + $('.event-filter-link').on('click', (e) => { + e.preventDefault(); + this.toggleFilter(e.currentTarget); + this.reloadActivities(); + }); + } + + updateTooltips() { + gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + } + + reloadActivities() { + $('.content_list').html(''); + Pager.init(20, true, false, this.updateTooltips); + } + + toggleFilter(sender) { + const $sender = $(sender); + const filter = $sender.attr('id').split('_')[0]; + + $('.event-filter .active').removeClass('active'); + Cookies.set('event_filter', filter); + + $sender.closest('li').toggleClass('active'); + } +} + +window.gl = window.gl || {}; +window.gl.Activities = Activities; diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6 deleted file mode 100644 index 648cb4d5d85..00000000000 --- a/app/assets/javascripts/activities.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-param-reassign, class-methods-use-this */ -/* global Pager */ -/* global Cookies */ - -((global) => { - class Activities { - constructor() { - Pager.init(20, true, false, this.updateTooltips); - $('.event-filter-link').on('click', (e) => { - e.preventDefault(); - this.toggleFilter(e.currentTarget); - this.reloadActivities(); - }); - } - - updateTooltips() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); - } - - reloadActivities() { - $('.content_list').html(''); - Pager.init(20, true, false, this.updateTooltips); - } - - toggleFilter(sender) { - const $sender = $(sender); - const filter = $sender.attr('id').split('_')[0]; - - $('.event-filter .active').removeClass('active'); - Cookies.set('event_filter', filter); - - $sender.closest('li').toggleClass('active'); - } - } - - global.Activities = Activities; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index aaed74d6073..34669dd13d6 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,64 +1,62 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ -(function() { - this.Admin = (function() { - function Admin() { - var modal, showBlacklistType; - $('input#user_force_random_password').on('change', function(elem) { - var elems; - elems = $('#user_password, #user_password_confirmation'); - if ($(this).attr('checked')) { - return elems.val('').attr('disabled', true); - } else { - return elems.removeAttr('disabled'); - } - }); - $('body').on('click', '.js-toggle-colors-link', function(e) { - e.preventDefault(); - return $('.js-toggle-colors-container').toggle(); - }); - $('.log-tabs a').click(function(e) { - e.preventDefault(); - return $(this).tab('show'); - }); - $('.log-bottom').click(function(e) { - var visible_log; - e.preventDefault(); - visible_log = $(".file-content:visible"); - return visible_log.animate({ - scrollTop: visible_log.find('ol').height() - }, "fast"); - }); - modal = $('.change-owner-holder'); - $('.change-owner-link').bind("click", function(e) { - e.preventDefault(); - $(this).hide(); - return modal.show(); - }); - $('.change-owner-cancel-link').bind("click", function(e) { - e.preventDefault(); - modal.hide(); - return $('.change-owner-link').show(); - }); - $('li.project_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); - }); - $('li.group_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); - }); - showBlacklistType = function() { - if ($("input[name='blacklist_type']:checked").val() === 'file') { - $('.blacklist-file').show(); - return $('.blacklist-raw').hide(); - } else { - $('.blacklist-file').hide(); - return $('.blacklist-raw').show(); - } - }; - $("input[name='blacklist_type']").click(showBlacklistType); - showBlacklistType(); - } +window.Admin = (function() { + function Admin() { + var modal, showBlacklistType; + $('input#user_force_random_password').on('change', function(elem) { + var elems; + elems = $('#user_password, #user_password_confirmation'); + if ($(this).attr('checked')) { + return elems.val('').attr('disabled', true); + } else { + return elems.removeAttr('disabled'); + } + }); + $('body').on('click', '.js-toggle-colors-link', function(e) { + e.preventDefault(); + return $('.js-toggle-colors-container').toggle(); + }); + $('.log-tabs a').click(function(e) { + e.preventDefault(); + return $(this).tab('show'); + }); + $('.log-bottom').click(function(e) { + var visible_log; + e.preventDefault(); + visible_log = $(".file-content:visible"); + return visible_log.animate({ + scrollTop: visible_log.find('ol').height() + }, "fast"); + }); + modal = $('.change-owner-holder'); + $('.change-owner-link').bind("click", function(e) { + e.preventDefault(); + $(this).hide(); + return modal.show(); + }); + $('.change-owner-cancel-link').bind("click", function(e) { + e.preventDefault(); + modal.hide(); + return $('.change-owner-link').show(); + }); + $('li.project_member').bind('ajax:success', function() { + return gl.utils.refreshCurrentPage(); + }); + $('li.group_member').bind('ajax:success', function() { + return gl.utils.refreshCurrentPage(); + }); + showBlacklistType = function() { + if ($("input[name='blacklist_type']:checked").val() === 'file') { + $('.blacklist-file').show(); + return $('.blacklist-raw').hide(); + } else { + $('.blacklist-file').hide(); + return $('.blacklist-raw').show(); + } + }; + $("input[name='blacklist_type']").click(showBlacklistType); + showBlacklistType(); + } - return Admin; - })(); -}).call(window); + return Admin; +})(); diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js new file mode 100644 index 00000000000..38a8317dbd7 --- /dev/null +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -0,0 +1,35 @@ +class AjaxLoadingSpinner { + static init() { + const $elements = $('.js-ajax-loading-spinner'); + + $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete); + } + + static ajaxBeforeSend(e) { + e.target.setAttribute('disabled', ''); + const iconElement = e.target.querySelector('i'); + // get first fa- icon + const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first(); + iconElement.dataset.icon = originalIcon; + AjaxLoadingSpinner.toggleLoadingIcon(iconElement); + $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + } + + static ajaxComplete(e) { + e.target.removeAttribute('disabled'); + const iconElement = e.target.querySelector('i'); + AjaxLoadingSpinner.toggleLoadingIcon(iconElement); + $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete); + } + + static toggleLoadingIcon(iconElement) { + const classList = iconElement.classList; + classList.toggle(iconElement.dataset.icon); + classList.toggle('fa-spinner'); + classList.toggle('fa-spin'); + } +} + +window.gl = window.gl || {}; +gl.AjaxLoadingSpinner = AjaxLoadingSpinner; diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 86e0ad89431..e5f36c84987 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,150 +1,148 @@ /* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ -(function() { - var Api = { - groupsPath: "/api/:version/groups.json", - groupPath: "/api/:version/groups/:id.json", - namespacesPath: "/api/:version/namespaces.json", - groupProjectsPath: "/api/:version/groups/:id/projects.json", - projectsPath: "/api/:version/projects.json?simple=true", - labelsPath: "/:namespace_path/:project_path/labels", - licensePath: "/api/:version/templates/licenses/:key", - gitignorePath: "/api/:version/templates/gitignores/:key", - gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", - dockerfilePath: "/api/:version/templates/dockerfiles/:key", - issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", - group: function(group_id, callback) { - var url = Api.buildUrl(Api.groupPath) - .replace(':id', group_id); - return $.ajax({ - url: url, - dataType: "json" - }).done(function(group) { - return callback(group); - }); - }, - // Return groups list. Filtered by query - groups: function(query, options, callback) { - var url = Api.buildUrl(Api.groupsPath); - return $.ajax({ - url: url, - data: $.extend({ - search: query, - per_page: 20 - }, options), - dataType: "json" - }).done(function(groups) { - return callback(groups); - }); - }, - // Return namespaces list. Filtered by query - namespaces: function(query, callback) { - var url = Api.buildUrl(Api.namespacesPath); - return $.ajax({ - url: url, - data: { - search: query, - per_page: 20 - }, - dataType: "json" - }).done(function(namespaces) { - return callback(namespaces); - }); - }, - // Return projects list. Filtered by query - projects: function(query, order, callback) { - var url = Api.buildUrl(Api.projectsPath); - return $.ajax({ - url: url, - data: { - search: query, - order_by: order, - per_page: 20 - }, - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); - }, - newLabel: function(namespace_path, project_path, data, callback) { - var url = Api.buildUrl(Api.labelsPath) - .replace(':namespace_path', namespace_path) - .replace(':project_path', project_path); - return $.ajax({ - url: url, - type: "POST", - data: { 'label': data }, - dataType: "json" - }).done(function(label) { - return callback(label); - }).error(function(message) { - return callback(message.responseJSON); - }); - }, - // Return group projects list. Filtered by query - groupProjects: function(group_id, query, callback) { - var url = Api.buildUrl(Api.groupProjectsPath) - .replace(':id', group_id); - return $.ajax({ - url: url, - data: { - search: query, - per_page: 20 - }, - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); - }, - // Return text for a specific license - licenseText: function(key, data, callback) { - var url = Api.buildUrl(Api.licensePath) - .replace(':key', key); - return $.ajax({ - url: url, - data: data - }).done(function(license) { - return callback(license); - }); - }, - gitignoreText: function(key, callback) { - var url = Api.buildUrl(Api.gitignorePath) - .replace(':key', key); - return $.get(url, function(gitignore) { - return callback(gitignore); - }); - }, - gitlabCiYml: function(key, callback) { - var url = Api.buildUrl(Api.gitlabCiYmlPath) - .replace(':key', key); - return $.get(url, function(file) { - return callback(file); - }); - }, - dockerfileYml: function(key, callback) { - var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - $.get(url, callback); - }, - issueTemplate: function(namespacePath, projectPath, key, type, callback) { - var url = Api.buildUrl(Api.issuableTemplatePath) - .replace(':key', key) - .replace(':type', type) - .replace(':project_path', projectPath) - .replace(':namespace_path', namespacePath); - $.ajax({ - url: url, - dataType: 'json' - }).done(function(file) { - callback(null, file); - }).error(callback); - }, - buildUrl: function(url) { - if (gon.relative_url_root != null) { - url = gon.relative_url_root + url; - } - return url.replace(':version', gon.api_version); +var Api = { + groupsPath: "/api/:version/groups.json", + groupPath: "/api/:version/groups/:id.json", + namespacesPath: "/api/:version/namespaces.json", + groupProjectsPath: "/api/:version/groups/:id/projects.json", + projectsPath: "/api/:version/projects.json?simple=true", + labelsPath: "/:namespace_path/:project_path/labels", + licensePath: "/api/:version/templates/licenses/:key", + gitignorePath: "/api/:version/templates/gitignores/:key", + gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", + dockerfilePath: "/api/:version/templates/dockerfiles/:key", + issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", + group: function(group_id, callback) { + var url = Api.buildUrl(Api.groupPath) + .replace(':id', group_id); + return $.ajax({ + url: url, + dataType: "json" + }).done(function(group) { + return callback(group); + }); + }, + // Return groups list. Filtered by query + groups: function(query, options, callback) { + var url = Api.buildUrl(Api.groupsPath); + return $.ajax({ + url: url, + data: $.extend({ + search: query, + per_page: 20 + }, options), + dataType: "json" + }).done(function(groups) { + return callback(groups); + }); + }, + // Return namespaces list. Filtered by query + namespaces: function(query, callback) { + var url = Api.buildUrl(Api.namespacesPath); + return $.ajax({ + url: url, + data: { + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(namespaces) { + return callback(namespaces); + }); + }, + // Return projects list. Filtered by query + projects: function(query, options, callback) { + var url = Api.buildUrl(Api.projectsPath); + return $.ajax({ + url: url, + data: $.extend({ + search: query, + per_page: 20, + membership: true + }, options), + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + newLabel: function(namespace_path, project_path, data, callback) { + var url = Api.buildUrl(Api.labelsPath) + .replace(':namespace_path', namespace_path) + .replace(':project_path', project_path); + return $.ajax({ + url: url, + type: "POST", + data: { 'label': data }, + dataType: "json" + }).done(function(label) { + return callback(label); + }).error(function(message) { + return callback(message.responseJSON); + }); + }, + // Return group projects list. Filtered by query + groupProjects: function(group_id, query, callback) { + var url = Api.buildUrl(Api.groupProjectsPath) + .replace(':id', group_id); + return $.ajax({ + url: url, + data: { + search: query, + per_page: 20 + }, + dataType: "json" + }).done(function(projects) { + return callback(projects); + }); + }, + // Return text for a specific license + licenseText: function(key, data, callback) { + var url = Api.buildUrl(Api.licensePath) + .replace(':key', key); + return $.ajax({ + url: url, + data: data + }).done(function(license) { + return callback(license); + }); + }, + gitignoreText: function(key, callback) { + var url = Api.buildUrl(Api.gitignorePath) + .replace(':key', key); + return $.get(url, function(gitignore) { + return callback(gitignore); + }); + }, + gitlabCiYml: function(key, callback) { + var url = Api.buildUrl(Api.gitlabCiYmlPath) + .replace(':key', key); + return $.get(url, function(file) { + return callback(file); + }); + }, + dockerfileYml: function(key, callback) { + var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); + $.get(url, callback); + }, + issueTemplate: function(namespacePath, projectPath, key, type, callback) { + var url = Api.buildUrl(Api.issuableTemplatePath) + .replace(':key', key) + .replace(':type', type) + .replace(':project_path', projectPath) + .replace(':namespace_path', namespacePath); + $.ajax({ + url: url, + dataType: 'json' + }).done(function(file) { + callback(null, file); + }).error(callback); + }, + buildUrl: function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root + url; } - }; + return url.replace(':version', gon.api_version); + } +}; - window.Api = Api; -}).call(window); +window.Api = Api; diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index 8e468faedbf..00000000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,246 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */ -/* global bp */ -/* global Cookies */ -/* global Flash */ -/* global ConfirmDangerModal */ -/* global AwardsHandler */ -/* global Aside */ - -function requireAll(context) { return context.keys().map(context); } - -window.$ = window.jQuery = require('jquery'); -require('jquery-ui/ui/autocomplete'); -require('jquery-ui/ui/draggable'); -require('jquery-ui/ui/effect-highlight'); -require('jquery-ui/ui/sortable'); -require('jquery-ujs'); -require('vendor/jquery.endless-scroll'); -require('vendor/jquery.highlight'); -require('vendor/jquery.waitforimages'); -require('vendor/jquery.caret'); -require('vendor/jquery.atwho'); -require('vendor/jquery.scrollTo'); -window.Cookies = require('js-cookie'); -require('./autosave'); -require('bootstrap/js/affix'); -require('bootstrap/js/alert'); -require('bootstrap/js/button'); -require('bootstrap/js/collapse'); -require('bootstrap/js/dropdown'); -require('bootstrap/js/modal'); -require('bootstrap/js/scrollspy'); -require('bootstrap/js/tab'); -require('bootstrap/js/transition'); -require('bootstrap/js/tooltip'); -require('bootstrap/js/popover'); -require('select2/select2.js'); -window.Pikaday = require('pikaday'); -window._ = require('underscore'); -window.Dropzone = require('dropzone'); -window.Sortable = require('vendor/Sortable'); -require('mousetrap'); -require('mousetrap/plugins/pause/mousetrap-pause'); -require('./shortcuts'); -require('./shortcuts_navigation'); -require('./shortcuts_dashboard_navigation'); -require('./shortcuts_issuable'); -require('./shortcuts_network'); -require('vendor/jquery.nicescroll'); -requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); -require('vendor/fuzzaldrin-plus'); -require('es6-promise').polyfill(); - -(function () { - document.addEventListener('beforeunload', function () { - // Unbind scroll events - $(document).off('scroll'); - // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); - }); - - window.addEventListener('hashchange', gl.utils.handleLocationHash); - window.addEventListener('load', function onLoad() { - window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); - }, false); - - $(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var $flash = $('.flash-container'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; - - // Set the default path for all cookies to GitLab's root directory - Cookies.defaults.path = gon.relative_url_root || '/'; - - // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); - if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); - } - }); - - // prevent default action for disabled buttons - $('.btn').click(function(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); - // Click a .js-select-on-focus field, select the contents - // Prevent a mouseup event from deselecting the input - }); - $('.remove-row').bind('ajax:success', function () { - $(this).tooltip('destroy') - .closest('li') - .fadeOut(); - }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); - }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); - }); - $('select.select2').select2({ - width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true - }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); - // Close select2 on escape - }); - // Initialize tooltips - $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; - $body.tooltip({ - selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (_, el) { - return $(el).data('placement') || 'bottom'; - } - }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); - // Form submitter - }); - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); - // Flash - if ($flash.length > 0) { - $flash.click(function () { - return $(this).fadeOut(); - }); - $flash.show(); - } - // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"]', this); - switch (e.type) { - case 'ajax:beforeSend': - case 'submit': - return buttons.disable(); - default: - return buttons.enable(); - } - }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); - } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); - } - }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); - // Commit show suppressed diff - }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); - // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); - $this.toggleClass('active'); - if ($this.hasClass('active')) { - notesHolders.show().find('.hide').show(); - } else { - notesHolders.hide(); - } - $this.trigger('blur'); - return e.preventDefault(); - }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); - e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); - }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; - if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); - if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); - } - } - }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; - bootstrapBreakpoint = bp.getBreakpointSize(); - if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); - } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - gl.awardsHandler = new AwardsHandler(); - new Aside(); - - gl.utils.initTimeagoTimeout(); - }); -}).call(window); diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 448e6e2cc78..88756884d16 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -1,25 +1,24 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */ -(function() { - this.Aside = (function() { - function Aside() { - $(document).off("click", "a.show-aside"); - $(document).on("click", 'a.show-aside', function(e) { - var btn, icon; - e.preventDefault(); - btn = $(e.currentTarget); - icon = btn.find('i'); - if (icon.hasClass('fa-angle-left')) { - btn.parent().find('section').hide(); - btn.parent().find('aside').fadeIn(); - return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); - } else { - btn.parent().find('aside').hide(); - btn.parent().find('section').fadeIn(); - return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); - } - }); - } - return Aside; - })(); -}).call(window); +window.Aside = (function() { + function Aside() { + $(document).off("click", "a.show-aside"); + $(document).on("click", 'a.show-aside', function(e) { + var btn, icon; + e.preventDefault(); + btn = $(e.currentTarget); + icon = btn.find('i'); + if (icon.hasClass('fa-angle-left')) { + btn.parent().find('section').hide(); + btn.parent().find('aside').fadeIn(); + return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); + } else { + btn.parent().find('aside').hide(); + btn.parent().find('section').fadeIn(); + return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); + } + }); + } + + return Aside; +})(); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index e55405135fb..8630b18a73f 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,62 +1,61 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ -(function() { - this.Autosave = (function() { - function Autosave(field, key) { - this.field = field; - if (key.join != null) { - key = key.join("/"); - } - this.key = "autosave/" + key; - this.field.data("autosave", this); - this.restore(); - this.field.on("input", (function(_this) { - return function() { - return _this.save(); - }; - })(this)); - } - Autosave.prototype.restore = function() { - var e, text; - if (window.localStorage == null) { - return; - } - try { - text = window.localStorage.getItem(this.key); - } catch (error) { - e = error; - return; - } - if ((text != null ? text.length : void 0) > 0) { - this.field.val(text); - } - return this.field.trigger("input"); - }; +window.Autosave = (function() { + function Autosave(field, key) { + this.field = field; + if (key.join != null) { + key = key.join("/"); + } + this.key = "autosave/" + key; + this.field.data("autosave", this); + this.restore(); + this.field.on("input", (function(_this) { + return function() { + return _this.save(); + }; + })(this)); + } - Autosave.prototype.save = function() { - var text; - if (window.localStorage == null) { - return; - } - text = this.field.val(); - if ((text != null ? text.length : void 0) > 0) { - try { - return window.localStorage.setItem(this.key, text); - } catch (error) {} - } else { - return this.reset(); - } - }; + Autosave.prototype.restore = function() { + var e, text; + if (window.localStorage == null) { + return; + } + try { + text = window.localStorage.getItem(this.key); + } catch (error) { + e = error; + return; + } + if ((text != null ? text.length : void 0) > 0) { + this.field.val(text); + } + return this.field.trigger("input"); + }; - Autosave.prototype.reset = function() { - if (window.localStorage == null) { - return; - } + Autosave.prototype.save = function() { + var text; + if (window.localStorage == null) { + return; + } + text = this.field.val(); + if ((text != null ? text.length : void 0) > 0) { try { - return window.localStorage.removeItem(this.key); + return window.localStorage.setItem(this.key, text); } catch (error) {} - }; + } else { + return this.reset(); + } + }; + + Autosave.prototype.reset = function() { + if (window.localStorage == null) { + return; + } + try { + return window.localStorage.removeItem(this.key); + } catch (error) {} + }; - return Autosave; - })(); -}).call(window); + return Autosave; +})(); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index a4ccb30e447..9349918f7a0 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,380 +1,518 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */ /* global Cookies */ -var emojiAliases = require('emoji-aliases'); - -(function() { - this.AwardsHandler = (function() { - var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence - function AwardsHandler() { - this.aliases = emojiAliases; - $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { - return function(e) { - e.stopPropagation(); - e.preventDefault(); - return _this.showEmojiMenu($(e.currentTarget)); - }; - })(this)); - $('html').on('click', function(e) { - var $target; - $target = $(e.target); - if (!$target.closest('.emoji-menu-content').length) { - $('.js-awards-block.current').removeClass('current'); - } - if (!$target.closest('.emoji-menu').length) { - if ($('.emoji-menu').is(':visible')) { - $('.js-add-award.is-active').removeClass('is-active'); - return $('.emoji-menu').removeClass('is-visible'); - } - } - }); - $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) { - return function(e) { - var $target, emoji; - e.preventDefault(); - $target = $(e.currentTarget); - emoji = $target.find('.icon').data('emoji'); - $target.closest('.js-awards-block').addClass('current'); - return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji); - }; - })(this)); +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from './behaviors/gl_emoji'; +import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; + +const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; +const requestAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.setTimeout; + +const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence + +let categoryMap = null; + +const categoryLabelMap = { + activity: 'Activity', + people: 'People', + nature: 'Nature', + food: 'Food', + travel: 'Travel', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags', +}; + +function buildCategoryMap() { + return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => { + const emojiInfo = emojiMap[emojiNameKey]; + if (currentCategoryMap[emojiInfo.category]) { + currentCategoryMap[emojiInfo.category].push(emojiNameKey); } - AwardsHandler.prototype.showEmojiMenu = function($addBtn) { - var $holder, $menu, url; - $menu = $('.emoji-menu'); - if ($addBtn.hasClass('js-note-emoji')) { - $addBtn.closest('.note').find('.js-awards-block').addClass('current'); - } else { - $addBtn.closest('.js-awards-block').addClass('current'); - } - if ($menu.length) { - $holder = $addBtn.closest('.js-award-holder'); - if ($menu.is('.is-visible')) { - $addBtn.removeClass('is-active'); - $menu.removeClass('is-visible'); - return $('#emoji_search').blur(); - } else { - $addBtn.addClass('is-active'); - this.positionMenu($menu, $addBtn); - $menu.addClass('is-visible'); - return $('#emoji_search').focus(); - } - } else { - $addBtn.addClass('is-loading is-active'); - url = this.getAwardMenuUrl(); - return this.createEmojiMenu(url, (function(_this) { - return function() { - $addBtn.removeClass('is-loading'); - $menu = $('.emoji-menu'); - _this.positionMenu($menu, $addBtn); - if (!_this.frequentEmojiBlockRendered) { - _this.renderFrequentlyUsedBlock(); - } - return setTimeout(function() { - $menu.addClass('is-visible'); - $('#emoji_search').focus(); - return _this.setupSearch(); - }, 200); - }; - })(this)); - } - }; - - AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) { - return $.get(awardMenuUrl, function(response) { - $('body').append(response); - return callback(); + return currentCategoryMap; + }, { + activity: [], + people: [], + nature: [], + food: [], + travel: [], + objects: [], + symbols: [], + flags: [], + }); +} + +function renderCategory(name, emojiList, opts = {}) { + return ` + <h5 class="emoji-menu-title"> + ${name} + </h5> + <ul class="clearfix emoji-menu-list ${opts.menuListClass}"> + ${emojiList.map(emojiName => ` + <li class="emoji-menu-list-item"> + <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> + ${glEmojiTag(emojiName, { + sprite: true, + })} + </button> + </li> + `).join('\n')} + </ul> + `; +} + +function AwardsHandler() { + this.eventListeners = []; + this.aliases = emojiAliases; + // If the user shows intent let's pre-build the menu + this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { + const $menu = $('.emoji-menu'); + if ($menu.length === 0) { + requestAnimationFrame(() => { + this.createEmojiMenu(); }); - }; - - AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { - var css, position; - position = $addBtn.data('position'); - // The menu could potentially be off-screen or in a hidden overflow element - // So we position the element absolute in the body - css = { - top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" - }; - if (position === 'right') { - css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; - $menu.addClass('is-aligned-right'); - } else { - css.left = ($addBtn.offset().left) + "px"; - $menu.removeClass('is-aligned-right'); - } - return $menu.css(css); - }; - - AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) { - if (checkMutuality == null) { - checkMutuality = true; - } - emoji = this.normilizeEmojiName(emoji); - this.postEmoji(awardUrl, emoji, (function(_this) { - return function() { - _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality); - return typeof callback === "function" ? callback() : void 0; - }; - })(this)); - return $('.emoji-menu').removeClass('is-visible'); - }; - - AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) { - var $emojiButton, counter; - if (checkForMutuality == null) { - checkForMutuality = true; - } - if (checkForMutuality) { - this.checkMutuality(votesBlock, emoji); - } - this.addEmojiToFrequentlyUsedList(emoji); - emoji = this.normilizeEmojiName(emoji); - $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent(); - if ($emojiButton.length > 0) { - if (this.isActive($emojiButton)) { - return this.decrementCounter($emojiButton, emoji); - } else { - counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text(), 10) + 1); - $emojiButton.addClass('active'); - this.addYouToUserList(votesBlock, emoji); - return this.animateEmoji($emojiButton); - } - } else { - votesBlock.removeClass('hidden'); - return this.createEmoji(votesBlock, emoji); - } - }; - - AwardsHandler.prototype.getVotesBlock = function() { - var currentBlock; - currentBlock = $('.js-awards-block.current'); - if (currentBlock.length) { - return currentBlock; - } else { - return $('.js-awards-block').eq(0); - } - }; - - AwardsHandler.prototype.getAwardUrl = function() { - return this.getVotesBlock().data('award-url'); - }; - - AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) { - var $emojiButton, awardUrl, isAlreadyVoted, mutualVote; - awardUrl = this.getAwardUrl(); - if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); - isAlreadyVoted = $emojiButton.hasClass('active'); - if (isAlreadyVoted) { - this.addAward(votesBlock, awardUrl, mutualVote, false); - } - } - }; - - AwardsHandler.prototype.isActive = function($emojiButton) { - return $emojiButton.hasClass('active'); - }; - - AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) { - var counter, counterNumber; - counter = $('.js-counter', $emojiButton); - counterNumber = parseInt(counter.text(), 10); - if (counterNumber > 1) { - counter.text(counterNumber - 1); - this.removeYouFromUserList($emojiButton, emoji); - } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); - counter.text('0'); - this.removeYouFromUserList($emojiButton, emoji); - if ($emojiButton.parents('.note').length) { - this.removeEmoji($emojiButton); - } - } else { - this.removeEmoji($emojiButton); - } - return $emojiButton.removeClass('active'); - }; - - AwardsHandler.prototype.removeEmoji = function($emojiButton) { - var $votesBlock; - $emojiButton.tooltip('destroy'); - $emojiButton.remove(); - $votesBlock = this.getVotesBlock(); - if ($votesBlock.find('.js-emoji-btn').length === 0) { - return $votesBlock.addClass('hidden'); - } - }; - - AwardsHandler.prototype.getAwardTooltip = function($awardBlock) { - return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; - }; - - AwardsHandler.prototype.toSentence = function(list) { - if (list.length <= 2) { - return list.join(' and '); - } - else { - return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1]; + } + // Prebuild the categoryMap + categoryMap = categoryMap || buildCategoryMap(); + }); + this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.showEmojiMenu($(e.currentTarget)); + }); + + this.registerEventListener('on', $('html'), 'click', (e) => { + const $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); + } + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + $('.emoji-menu').removeClass('is-visible'); } - }; - - AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) { - var authors, awardBlock, newAuthors, originalTitle; - awardBlock = $emojiButton; - originalTitle = this.getAwardTooltip(awardBlock); - authors = originalTitle.split(FROM_SENTENCE_REGEX); - authors.splice(authors.indexOf('You'), 1); - return awardBlock - .closest('.js-emoji-btn') - .removeData('title') - .removeAttr('data-title') - .removeAttr('data-original-title') - .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); - }; - - AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) { - var awardBlock, origTitle, users; - awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); - origTitle = this.getAwardTooltip(awardBlock); - users = []; - if (origTitle) { - users = origTitle.trim().split(FROM_SENTENCE_REGEX); + } + }); + this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const $glEmojiElement = $target.find('gl-emoji'); + const $spriteIconElement = $target.find('.icon'); + const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + $target.closest('.js-awards-block').addClass('current'); + return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + }); +} + +AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) { + element[method].call(element, ...args); + this.eventListeners.push({ + element, + args, + }); +}; + +AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); + } else { + $addBtn.closest('.js-awards-block').addClass('current'); + } + + const $menu = $('.emoji-menu'); + if ($menu.length) { + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + $('#emoji_search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + $('#emoji_search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + this.createEmojiMenu(() => { + const $createdMenu = $('.emoji-menu'); + $addBtn.removeClass('is-loading'); + this.positionMenu($createdMenu, $addBtn); + return setTimeout(() => { + $createdMenu.addClass('is-visible'); + $('#emoji_search').focus(); + }, 200); + }); + } +}; + +// Create the emoji menu with the first category of emojis. +// Then render the remaining categories of emojis one by one to avoid jank. +AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { + if (this.isCreatingEmojiMenu) { + return; + } + this.isCreatingEmojiMenu = true; + + // Render the first category + categoryMap = categoryMap || buildCategoryMap(); + const categoryNameKey = Object.keys(categoryMap)[0]; + const emojisInCategory = categoryMap[categoryNameKey]; + const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + + // Render the frequently used + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + let frequentlyUsedCatgegory = ''; + if (frequentlyUsedEmojis.length > 0) { + frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + menuListClass: 'frequent-emojis', + }); + } + + const emojiMenuMarkup = ` + <div class="emoji-menu"> + <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> + + <div class="emoji-menu-content"> + ${frequentlyUsedCatgegory} + ${firstCategory} + </div> + </div> + `; + + document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + + this.addRemainingEmojiMenuCategories(); + this.setupSearch(); + if (callback) { + callback(); + } +}; + +AwardsHandler + .prototype + .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() { + if (this.isAddingRemainingEmojiMenuCategories) { + return; + } + this.isAddingRemainingEmojiMenuCategories = true; + + categoryMap = categoryMap || buildCategoryMap(); + + // Avoid the jank and render the remaining categories separately + // This will take more time, but makes UI more responsive + const menu = document.querySelector('.emoji-menu'); + const emojiContentElement = menu.querySelector('.emoji-menu-content'); + const remainingCategories = Object.keys(categoryMap).slice(1); + const allCategoriesAddedPromise = remainingCategories.reduce( + (promiseChain, categoryNameKey) => + promiseChain.then(() => + new Promise((resolve) => { + const emojisInCategory = categoryMap[categoryNameKey]; + const categoryMarkup = renderCategory( + categoryLabelMap[categoryNameKey], + emojisInCategory, + ); + requestAnimationFrame(() => { + emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup); + resolve(); + }); + }), + ), + Promise.resolve(), + ); + + allCategoriesAddedPromise.then(() => { + // Used for tests + // We check for the menu in case it was destroyed in the meantime + if (menu) { + menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); } - users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); - }; - - AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { - var $emojiButton, buttonHtml, emojiCssClass; - emojiCssClass = this.resolveNameToCssClass(emoji); - buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; - $emojiButton = $(buttonHtml); - $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); + }); + }; + +AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) { + const position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body + const css = { + top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, + }; + if (position === 'right') { + css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; + $menu.addClass('is-aligned-right'); + } else { + css.left = `${$addBtn.offset().left}px`; + $menu.removeClass('is-aligned-right'); + } + return $menu.css(css); +}; + +AwardsHandler.prototype.addAward = function addAward( + votesBlock, + awardUrl, + emoji, + checkMutuality, + callback, +) { + const normalizedEmoji = this.normalizeEmojiName(emoji); + this.postEmoji(awardUrl, normalizedEmoji, () => { + this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); + return typeof callback === 'function' ? callback() : undefined; + }); + return $('.emoji-menu').removeClass('is-visible'); +}; + +AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( + votesBlock, + emoji, + checkForMutuality, +) { + if (checkForMutuality || checkForMutuality === null) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + const normalizedEmoji = this.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + this.decrementCounter($emojiButton, normalizedEmoji); + } else { + const counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text(), 10) + 1); + $emojiButton.addClass('active'); + this.addYouToUserList(votesBlock, normalizedEmoji); this.animateEmoji($emojiButton); - $('.award-control').tooltip(); - return votesBlock.removeClass('current'); - }; - - AwardsHandler.prototype.animateEmoji = function($emoji) { - var className = 'pulse animated once short'; - $emoji.addClass(className); + } + } else { + votesBlock.removeClass('hidden'); + this.createEmoji(votesBlock, normalizedEmoji); + } +}; + +AwardsHandler.prototype.getVotesBlock = function getVotesBlock() { + const currentBlock = $('.js-awards-block.current'); + let resultantVotesBlock = currentBlock; + if (currentBlock.length === 0) { + resultantVotesBlock = $('.js-awards-block').eq(0); + } + + return resultantVotesBlock; +}; + +AwardsHandler.prototype.getAwardUrl = function getAwardUrl() { + return this.getVotesBlock().data('award-url'); +}; + +AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) { + const awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); + const isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.addAward(votesBlock, awardUrl, mutualVote, false); + } + } +}; + +AwardsHandler.prototype.isActive = function isActive($emojiButton) { + return $emojiButton.hasClass('active'); +}; + +AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { + const counter = $('.js-counter', $emojiButton); + const counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeYouFromUserList($emojiButton); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeYouFromUserList($emojiButton); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { + this.removeEmoji($emojiButton); + } + return $emojiButton.removeClass('active'); +}; + +AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) { + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + const $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + $votesBlock.addClass('hidden'); + } +}; + +AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; +}; + +AwardsHandler.prototype.toSentence = function toSentence(list) { + let sentence; + if (list.length <= 2) { + sentence = list.join(' and '); + } else { + sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + } + + return sentence; +}; + +AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) { + const awardBlock = $emojiButton; + const originalTitle = this.getAwardTooltip(awardBlock); + const authors = originalTitle.split(FROM_SENTENCE_REGEX); + authors.splice(authors.indexOf('You'), 1); + return awardBlock + .closest('.js-emoji-btn') + .removeData('title') + .removeAttr('data-title') + .removeAttr('data-original-title') + .attr('title', this.toSentence(authors)) + .tooltip('fixTitle'); +}; + +AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) { + const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + const origTitle = this.getAwardTooltip(awardBlock); + let users = []; + if (origTitle) { + users = origTitle.trim().split(FROM_SENTENCE_REGEX); + } + users.unshift('You'); + return awardBlock + .attr('title', this.toSentence(users)) + .tooltip('fixTitle'); +}; + +AwardsHandler + .prototype + .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) { + const buttonHtml = ` + <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> + ${glEmojiTag(emojiName)} + <span class="award-control-text js-counter">1</span> + </button> + `; + const $emojiButton = $(buttonHtml); + $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName); + this.animateEmoji($emojiButton); + $('.award-control').tooltip(); + votesBlock.removeClass('current'); + }; + +AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) { + const className = 'pulse animated once short'; + $emoji.addClass(className); + + this.registerEventListener('on', $emoji, animationEndEventString, (e) => { + $(e.currentTarget).removeClass(className); + }); +}; + +AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { + if ($('.emoji-menu').length) { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + } + this.createEmojiMenu(() => { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + }); +}; + +AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) { + return $.post(awardUrl, { + name: emoji, + }, (data) => { + if (data.ok) { + callback(); + } + }); +}; + +AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { + return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); +}; + +AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { + const options = { + scrollTop: $('.awards').offset().top - 110, + }; + return $('body, html').animate(options, 200); +}; + +AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) { + return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji; +}; + +AwardsHandler + .prototype + .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) { + if (isEmojiNameValid(emoji)) { + this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); + Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); + } + }; - $emoji.on('webkitAnimationEnd animationEnd', function() { - $(this).removeClass(className); - }); - }; +AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() { + return this.frequentlyUsedEmojis || (() => { + const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); + this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( + inputName => isEmojiNameValid(inputName), + ); - AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { - if ($('.emoji-menu').length) { - return this.createEmoji_(votesBlock, emoji); - } - return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) { - return function() { - return _this.createEmoji_(votesBlock, emoji); - }; - })(this)); - }; - - AwardsHandler.prototype.getAwardMenuUrl = function() { - return gon.award_menu_url; - }; - - AwardsHandler.prototype.resolveNameToCssClass = function(emoji) { - var emojiIcon, unicodeName; - emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']"); - if (emojiIcon.length > 0) { - unicodeName = emojiIcon.data('unicode-name'); - } else { - // Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); - } - return "emoji-" + unicodeName; - }; - - AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) { - return $.post(awardUrl, { - name: emoji - }, function(data) { - if (data.ok) { - return callback(); - } - }); - }; - - AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) { - return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']"); - }; - - AwardsHandler.prototype.scrollToAwards = function() { - var options; - options = { - scrollTop: $('.awards').offset().top - 110 - }; - return $('body, html').animate(options, 200); - }; - - AwardsHandler.prototype.normilizeEmojiName = function(emoji) { - return this.aliases[emoji] || emoji; - }; - - AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) { - var frequentlyUsedEmojis; - frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - frequentlyUsedEmojis.push(emoji); - Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); - }; - - AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { - var frequentlyUsedEmojis; - frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); - return _.compact(_.uniq(frequentlyUsedEmojis)); - }; - - AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { - var emoji, frequentlyUsedEmojis, i, len, ul; - if (Cookies.get('frequently_used_emojis')) { - frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); - for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) { - emoji = frequentlyUsedEmojis[i]; - $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul); - } - $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); - } - return this.frequentEmojiBlockRendered = true; - }; - - AwardsHandler.prototype.setupSearch = function() { - return $('input.emoji-search').on('keyup', (function(_this) { - return function(ev) { - var found_emojis, h5, term, ul; - term = $(ev.target).val(); - // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove(); - if (term) { - // Generate a search result block - h5 = $('<h5 class="emoji-search" />').text('Search results'); - found_emojis = _this.searchEmojis(term).show(); - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); - $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - return $('.emoji-menu-content').append(h5).append(ul); - } else { - return $('.emoji-menu-content').children().show(); - } - }; - })(this)); - }; - - AwardsHandler.prototype.searchEmojis = function(term) { - return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone(); - }; - - return AwardsHandler; + return this.frequentlyUsedEmojis; })(); -}).call(window); +}; + +AwardsHandler.prototype.setupSearch = function setupSearch() { + this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { + const term = $(e.target).val().trim(); + // Clean previous search results + $('ul.emoji-menu-search, h5.emoji-search').remove(); + if (term.length > 0) { + // Generate a search result block + const h5 = $('<h5 class="emoji-search" />').text('Search results'); + const foundEmojis = this.searchEmojis(term).show(); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + $('.emoji-menu-content').append(h5).append(ul); + } else { + $('.emoji-menu-content').children().show(); + } + }); +}; + +AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { + const safeTerm = term.toLowerCase(); + + const namesMatchingAlias = []; + Object.keys(emojiAliases).forEach((alias) => { + if (alias.indexOf(safeTerm) >= 0) { + namesMatchingAlias.push(emojiAliases[alias]); + } + }); + const $matchingElements = namesMatchingAlias.concat(safeTerm) + .reduce( + ($result, searchTerm) => + $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)), + $([]), + ); + return $matchingElements.closest('li').clone(); +}; + +AwardsHandler.prototype.destroy = function destroy() { + this.eventListeners.forEach((entry) => { + entry.element.off.call(entry.element, ...entry.args); + }); + $('.emoji-menu').remove(); +}; + +export default AwardsHandler; diff --git a/app/assets/javascripts/behaviors/bind_in_out.js b/app/assets/javascripts/behaviors/bind_in_out.js new file mode 100644 index 00000000000..886f127b06b --- /dev/null +++ b/app/assets/javascripts/behaviors/bind_in_out.js @@ -0,0 +1,47 @@ +class BindInOut { + constructor(bindIn, bindOut) { + this.in = bindIn; + this.out = bindOut; + + this.eventWrapper = {}; + this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change'; + } + + addEvents() { + this.eventWrapper.updateOut = this.updateOut.bind(this); + + this.in.addEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + updateOut() { + this.out.textContent = this.in.value; + + return this; + } + + removeEvents() { + this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + static initAll() { + const ins = document.querySelectorAll('*[data-bind-in]'); + + return [].map.call(ins, anIn => BindInOut.init(anIn)); + } + + static init(anIn, anOut) { + const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`); + + if (!out) return null; + + const bindInOut = new BindInOut(anIn, out); + + return bindInOut.addEvents().updateOut(); + } +} + +export default BindInOut; diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js new file mode 100644 index 00000000000..19a607309e4 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -0,0 +1,116 @@ +import installCustomElements from 'document-register-element'; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map'; +import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported'; + +installCustomElements(window); + +const generatedUnicodeSupportMap = getUnicodeSupportMap(); + +function emojiImageTag(name, src) { + return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; +} + +function assembleFallbackImageSrc(inputName) { + let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? + emojiAliases[inputName] : inputName; + let emojiInfo = emojiMap[name]; + // Fallback to question mark for unknown emojis + if (!emojiInfo) { + name = 'grey_question'; + emojiInfo = emojiMap[name]; + } + const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`; + + return fallbackImageSrc; +} +const glEmojiTagDefaults = { + sprite: false, + forceFallback: false, +}; +function glEmojiTag(inputName, options) { + const opts = Object.assign({}, glEmojiTagDefaults, options); + let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? + emojiAliases[inputName] : inputName; + let emojiInfo = emojiMap[name]; + // Fallback to question mark for unknown emojis + if (!emojiInfo) { + name = 'grey_question'; + emojiInfo = emojiMap[name]; + } + + const fallbackImageSrc = assembleFallbackImageSrc(name); + const fallbackSpriteClass = `emoji-${name}`; + + const classList = []; + if (opts.forceFallback && opts.sprite) { + classList.push('emoji-icon'); + classList.push(fallbackSpriteClass); + } + const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; + const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; + let contents = emojiInfo.moji; + if (opts.forceFallback && !opts.sprite) { + contents = emojiImageTag(name, fallbackImageSrc); + } + + return ` + <gl-emoji + ${classAttribute} + data-name="${name}" + data-fallback-src="${fallbackImageSrc}" + ${fallbackSpriteAttribute} + data-unicode-version="${emojiInfo.unicodeVersion}" + > + ${contents} + </gl-emoji> + `; +} + +function installGlEmojiElement() { + const GlEmojiElementProto = Object.create(HTMLElement.prototype); + GlEmojiElementProto.createdCallback = function createdCallback() { + const emojiUnicode = this.textContent.trim(); + const { + name, + unicodeVersion, + fallbackSrc, + fallbackSpriteClass, + } = this.dataset; + + const isEmojiUnicode = this.childNodes && Array.prototype.every.call( + this.childNodes, + childNode => childNode.nodeType === 3, + ); + const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; + const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; + + if ( + isEmojiUnicode && + !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + ) { + // CSS sprite fallback takes precedence over image fallback + if (hasCssSpriteFalback) { + // IE 11 doesn't like adding multiple at once :( + this.classList.add('emoji-icon'); + this.classList.add(fallbackSpriteClass); + } else if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = assembleFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + } + }; + + document.registerElement('gl-emoji', { + prototype: GlEmojiElementProto, + }); +} + +export { + installGlEmojiElement, + glEmojiTag, + emojiImageTag, +}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js new file mode 100644 index 00000000000..be4aeb32c46 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js @@ -0,0 +1,11 @@ +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; + +function isEmojiNameValid(inputName) { + const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? + emojiAliases[inputName] : inputName; + + return name && emojiMap[name]; +} + +export default isEmojiNameValid; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js new file mode 100644 index 00000000000..5e3c45f7e92 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -0,0 +1,121 @@ +import spreadString from './spread_string'; + +// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ +const flagACodePoint = 127462; // parseInt('1F1E6', 16) +const flagZCodePoint = 127487; // parseInt('1F1FF', 16) +function isFlagEmoji(emojiUnicode) { + const cp = emojiUnicode.codePointAt(0); + // Length 4 because flags are made of 2 characters which are surrogate pairs + return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; +} + +// Chrome <57 renders keycaps oddly +// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 +// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png +function isKeycapEmoji(emojiUnicode) { + return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; +} + +// Check for a skin tone variation emoji which aren't always supported +const tone1 = 127995;// parseInt('1F3FB', 16) +const tone5 = 127999;// parseInt('1F3FF', 16) +function isSkinToneComboEmoji(emojiUnicode) { + return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }); +} + +// macOS supports most skin tone emoji's but +// doesn't support the skin tone versions of horse racing +const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +function isHorceRacingSkinToneComboEmoji(emojiUnicode) { + return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode); +} + +// Check for `family_*`, `kiss_*`, `couple_*` +// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these +const zwj = 8205; // parseInt('200D', 16) +const personStartCodePoint = 128102; // parseInt('1F466', 16) +const personEndCodePoint = 128105; // parseInt('1F469', 16) +function isPersonZwjEmoji(emojiUnicode) { + let hasPersonEmoji = false; + let hasZwj = false; + spreadString(emojiUnicode).forEach((character) => { + const cp = character.codePointAt(0); + if (cp === zwj) { + hasZwj = true; + } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { + hasPersonEmoji = true; + } + }); + + return hasPersonEmoji && hasZwj; +} + +// Helper so we don't have to run `isFlagEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isFlagResult = isFlagEmoji(emojiUnicode); + return ( + (unicodeSupportMap.flag && isFlagResult) || + !isFlagResult + ); +} + +// Helper so we don't have to run `isSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { + const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.skinToneModifier && isSkinToneResult) || + !isSkinToneResult + ); +} + +// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || + !isHorseRacingSkinToneResult + ); +} + +// Helper so we don't have to run `isPersonZwjEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); + return ( + (unicodeSupportMap.personZwj && isPersonZwjResult) || + !isPersonZwjResult + ); +} + +// Takes in a support map and determines whether +// the given unicode emoji is supported on the platform. +// +// Combines all the edge case tests into a one-stop shop method +function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { + const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + unicodeSupportMap.meta.chromeVersion < 57; + + // For comments about each scenario, see the comments above each individual respective function + return unicodeSupportMap[unicodeVersion] && + !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && + checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && + checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); +} + +export { + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js new file mode 100644 index 00000000000..327764ec6e9 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js @@ -0,0 +1,50 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known +function knownCharCodeAt(givenString, index) { + const str = `${givenString}`; + const end = str.length; + + const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + let idx = index; + while ((surrogatePairs.exec(str)) != null) { + const li = surrogatePairs.lastIndex; + if (li - 2 < idx) { + idx += 1; + } else { + break; + } + } + + if (idx >= end || idx < 0) { + return NaN; + } + + const code = str.charCodeAt(idx); + + let high; + let low; + if (code >= 0xD800 && code <= 0xDBFF) { + high = code; + low = str.charCodeAt(idx + 1); + // Go one further, since one of the "characters" is part of a surrogate pair + return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + } + return code; +} + +// See http://stackoverflow.com/a/38901550/796832 +// ES5/PhantomJS compatible version of spreading a string +// +// [...'foo'] -> ['f', 'o', 'o'] +// [...'🖐🏿'] -> ['🖐', '🏿'] +function spreadString(str) { + const arr = []; + let i = 0; + while (!isNaN(knownCharCodeAt(str, i))) { + const codePoint = knownCharCodeAt(str, i); + arr.push(String.fromCodePoint(codePoint)); + i += 1; + } + return arr; +} + +export default spreadString; diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js new file mode 100644 index 00000000000..aa522e20c36 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js @@ -0,0 +1,161 @@ +const unicodeSupportTestMap = { + // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ + // occupationZwj: '\u{1F468}\u{200D}\u{1F393}', + // woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ + // sexZwj: '\u{1F6B4}\u{200D}\u{2640}', + // family_mwgb + // Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_` + personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', + // horse_racing_tone5 + // Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds + horseRacing: '\u{1F3C7}\u{1F3FF}', + // US flag, http://emojipedia.org/flags/ + flag: '\u{1F1FA}\u{1F1F8}', + // http://emojipedia.org/modifiers/ + skinToneModifier: [ + // spy_tone5 + '\u{1F575}\u{1F3FF}', + // person_with_ball_tone5 + '\u{26F9}\u{1F3FF}', + // angel_tone5 + '\u{1F47C}\u{1F3FF}', + ], + // rofl, http://emojipedia.org/unicode-9.0/ + '9.0': '\u{1F923}', + // metal, http://emojipedia.org/unicode-8.0/ + '8.0': '\u{1F918}', + // spy, http://emojipedia.org/unicode-7.0/ + '7.0': '\u{1F575}', + // expressionless, http://emojipedia.org/unicode-6.1/ + 6.1: '\u{1F611}', + // japanese_goblin, http://emojipedia.org/unicode-6.0/ + '6.0': '\u{1F47A}', + // sailboat, http://emojipedia.org/unicode-5.2/ + 5.2: '\u{26F5}', + // mahjong, http://emojipedia.org/unicode-5.1/ + 5.1: '\u{1F004}', + // gear, http://emojipedia.org/unicode-4.1/ + 4.1: '\u{2699}', + // zap, http://emojipedia.org/unicode-4.0/ + '4.0': '\u{26A1}', + // recycle, http://emojipedia.org/unicode-3.2/ + 3.2: '\u{267B}', + // information_source, http://emojipedia.org/unicode-3.0/ + '3.0': '\u{2139}', + // heart, http://emojipedia.org/unicode-1.1/ + 1.1: '\u{2764}', +}; + +function checkPixelInImageDataArray(pixelOffset, imageDataArray) { + // `4 *` because RGBA + const indexOffset = 4 * pixelOffset; + const hasColor = imageDataArray[indexOffset + 0] || + imageDataArray[indexOffset + 1] || + imageDataArray[indexOffset + 2]; + const isVisible = imageDataArray[indexOffset + 3]; + // Check for some sort of color other than black + if (hasColor && isVisible) { + return true; + } + return false; +} + +const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./); +const isChrome = chromeMatches && chromeMatches.length > 0; +const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10); + +// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/ +// See 32px, https://i.imgur.com/htY6Zym.png +// See 16px, https://i.imgur.com/FPPsIF8.png +const fontSize = 16; +function generateUnicodeSupportMap(testMap) { + const testMapKeys = Object.keys(testMap); + const numTestEntries = testMapKeys + .reduce((list, testKey) => list.concat(testMap[testKey]), []).length; + + const canvas = document.createElement('canvas'); + (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; + const ctx = canvas.getContext('2d'); + canvas.width = (2 * fontSize); + canvas.height = (numTestEntries * fontSize); + ctx.fillStyle = '#000000'; + ctx.textBaseline = 'middle'; + ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; + // Write each emoji to the canvas vertically + let writeIndex = 0; + testMapKeys.forEach((testKey) => { + const testEntry = testMap[testKey]; + [].concat(testEntry).forEach((emojiUnicode) => { + ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); + writeIndex += 1; + }); + }); + + // Read from the canvas + const resultMap = {}; + let readIndex = 0; + testMapKeys.forEach((testKey) => { + const testEntry = testMap[testKey]; + // This needs to be a `reduce` instead of `every` because we need to + // keep the `readIndex` in sync from the writes by running all entries + const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { + // Sample along the vertical-middle for a couple of characters + const imageData = ctx.getImageData( + 0, + (readIndex * fontSize) + (fontSize / 2), + 2 * fontSize, + 1, + ).data; + + let isValidEmoji = false; + for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { + const isLookingAtFirstChar = currentPixel < fontSize; + const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); + // Check for the emoji somewhere along the row + if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { + isValidEmoji = true; + + // Check to see that nothing is rendered next to the first character + // to ensure that the ZWJ sequence rendered as one piece + } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { + isValidEmoji = false; + break; + } + } + + readIndex += 1; + return isSatisfied && isValidEmoji; + }, true); + + resultMap[testKey] = isTestSatisfied; + }); + + resultMap.meta = { + isChrome, + chromeVersion, + }; + + return resultMap; +} + +function getUnicodeSupportMap() { + let unicodeSupportMap; + const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + try { + unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); + } catch (err) { + // swallow + } + if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { + unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); + window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); + window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); + } + + return unicodeSupportMap; +} + +export { + getUnicodeSupportMap, + generateUnicodeSupportMap, +}; diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index a7e68ae5cb9..626f3503c91 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -6,7 +6,7 @@ // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // is submitted. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 6b21695d082..eb7143f5b1a 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -4,7 +4,7 @@ // When called on a form with input fields with the `required` attribute, the // form's submit button will be disabled until all required fields have values. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index a7181904ac9..92f3bb3ff52 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -22,8 +22,12 @@ // %div.js-toggle-content // $('body').on('click', '.js-toggle-button', function(e) { - e.preventDefault(); toggleContainer($(this).closest('.js-toggle-container')); + + const targetTag = e.target.tagName.toLowerCase(); + if (targetTag === 'a' || targetTag === 'button') { + e.preventDefault(); + } }); // If we're accessing a permalink, ensure it is not inside a diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js index ec1c018424d..ec1c018424d 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js index d4f60cc6ecd..d4f60cc6ecd 100644 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js index 9cee79fa5d5..9cee79fa5d5 100644 --- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 5f14ff40eee..8f6bf162d6e 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -36,7 +36,7 @@ this.removeFile(file); }); return this.on('sending', function(file, xhr, formData) { - formData.append('target_branch', form.find('.js-target-branch').val()); + formData.append('target_branch', form.find('input[name="target_branch"]').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('commit_message', form.find('.js-commit-message').val()); }); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js index c5067b0feae..c5067b0feae 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js new file mode 100644 index 00000000000..c8f68860fbd --- /dev/null +++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js @@ -0,0 +1,35 @@ +const lineNumberRe = /^L[0-9]+/; + +const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { + const hash = gl.utils.getLocationHash(); + if (hash && lineNumberRe.test(hash)) { + const hashUrlString = `#${hash}`; + + [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => { + const baseHref = permalinkButton.getAttribute('data-original-href') || (() => { + const href = permalinkButton.getAttribute('href'); + permalinkButton.setAttribute('data-original-href', href); + return href; + })(); + permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`); + }); + } +}; + +function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) { + const updateBlameAndBlobPermalinkCb = () => { + // Wait for the hash to update from the LineHighlighter callback + setTimeout(() => { + updateLineNumbersOnBlobPermalinks(elementsToUpdate); + }, 0); + }; + + blobContentHolder.addEventListener('click', (e) => { + if (e.target.matches(lineNumberSelector)) { + updateBlameAndBlobPermalinkCb(); + } + }); + updateBlameAndBlobPermalinkCb(); +} + +export default BlobLinePermalinkUpdater; diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js new file mode 100644 index 00000000000..95517f51b1c --- /dev/null +++ b/app/assets/javascripts/blob/create_branch_dropdown.js @@ -0,0 +1,88 @@ +class CreateBranchDropdown { + constructor(el, targetBranchDropdown) { + this.targetBranchDropdown = targetBranchDropdown; + this.el = el; + this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back'); + this.cancelButton = this.el.querySelector('.js-cancel-branch-btn'); + this.newBranchField = this.el.querySelector('#new_branch_name'); + this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn'); + + this.newBranchCreateButton.setAttribute('disabled', ''); + + this.addBindings(); + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + + cleanup() { + this.cleanBindings(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + cleanBindings() { + this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper); + this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper); + this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper); + this.dropdownBack.removeEventListener('click', this.resetFormWrapper); + this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper); + this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper); + } + + addBindings() { + this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this); + this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this); + this.resetFormWrapper = this.resetForm.bind(this); + this.handleCancelClickWrapper = this.handleCancelClick.bind(this); + this.createBranchWrapper = this.createBranch.bind(this); + + this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper); + this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper); + this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper); + this.dropdownBack.addEventListener('click', this.resetFormWrapper); + this.cancelButton.addEventListener('click', this.handleCancelClickWrapper); + this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper); + } + + handleCancelClick(e) { + e.preventDefault(); + e.stopPropagation(); + + this.resetForm(); + this.dropdownBack.click(); + } + + handleNewBranchKeydown(e) { + const keyCode = e.which; + const ENTER_KEYCODE = 13; + if (keyCode === ENTER_KEYCODE) { + this.createBranch(e); + } + } + + enableBranchCreateButton() { + if (this.newBranchField.value !== '') { + this.newBranchCreateButton.removeAttribute('disabled'); + } else { + this.newBranchCreateButton.setAttribute('disabled', ''); + } + } + + resetForm() { + this.newBranchField.value = ''; + this.enableBranchCreateButtonWrapper(); + } + + createBranch(e) { + e.preventDefault(); + + if (this.newBranchCreateButton.getAttribute('disabled') === '') { + return; + } + const newBranchName = this.newBranchField.value; + this.targetBranchDropdown.setNewBranch(newBranchName); + this.resetForm(); + } +} + +window.gl = window.gl || {}; +gl.CreateBranchDropdown = CreateBranchDropdown; diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js new file mode 100644 index 00000000000..216f069ef71 --- /dev/null +++ b/app/assets/javascripts/blob/target_branch_dropdown.js @@ -0,0 +1,152 @@ +/* eslint-disable class-methods-use-this */ +const SELECT_ITEM_MSG = 'Select'; + +class TargetBranchDropDown { + constructor(dropdown) { + this.dropdown = dropdown; + this.$dropdown = $(dropdown); + this.fieldName = this.dropdown.getAttribute('data-field-name'); + this.form = this.dropdown.closest('form'); + this.createDropdown(); + } + + static bootstrap() { + const dropdowns = document.querySelectorAll('.js-project-branches-dropdown'); + [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown)); + } + + createDropdown() { + const self = this; + this.$dropdown.glDropdown({ + selectable: true, + filterable: true, + search: { + fields: ['title'], + }, + data: (term, callback) => $.ajax({ + url: self.dropdown.getAttribute('data-refs-url'), + data: { + ref: self.dropdown.getAttribute('data-ref'), + show_all: true, + }, + dataType: 'json', + }).done(refs => callback(self.dropdownData(refs))), + toggleLabel(item, el) { + if (el.is('.is-active')) { + return item.text; + } + return SELECT_ITEM_MSG; + }, + clicked(item, el, e) { + e.preventDefault(); + self.onClick.call(self); + }, + fieldName: self.fieldName, + }); + return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this); + } + + onClick() { + this.enableSubmit(); + this.$dropdown.trigger('change.branch'); + } + + enableSubmit() { + const submitBtn = this.form.querySelector('[type="submit"]'); + if (this.branchInput && this.branchInput.value) { + submitBtn.removeAttribute('disabled'); + } else { + submitBtn.setAttribute('disabled', ''); + } + } + + dropdownData(refs) { + const branchList = this.dropdownItems(refs); + this.cachedRefs = refs; + this.addDefaultBranch(branchList); + this.addNewBranch(branchList); + return { Branches: branchList }; + } + + dropdownItems(refs) { + return refs.map(this.dropdownItem); + } + + dropdownItem(ref) { + return { id: ref, text: ref, title: ref }; + } + + addDefaultBranch(branchList) { + // when no branch is selected do nothing + if (!this.branchInput) { + return; + } + + const branchInputVal = this.branchInput.value; + const currentBranchIndex = this.searchBranch(branchList, branchInputVal); + + if (currentBranchIndex === -1) { + this.unshiftBranch(branchList, this.dropdownItem(branchInputVal)); + } + } + + addNewBranch(branchList) { + if (this.newBranch) { + this.unshiftBranch(branchList, this.newBranch); + } + } + + searchBranch(branchList, branchName) { + return _.findIndex(branchList, el => branchName === el.id); + } + + unshiftBranch(branchList, branch) { + const branchIndex = this.searchBranch(branchList, branch.id); + + if (branchIndex === -1) { + branchList.unshift(branch); + } + } + + setNewBranch(newBranchName) { + this.newBranch = this.dropdownItem(newBranchName); + this.refreshData(); + this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName)); + } + + refreshData() { + this.glDropdown.fullData = this.dropdownData(this.cachedRefs); + this.clearFilter(); + } + + clearFilter() { + // apply an empty filter in order to refresh the data + this.glDropdown.filter.filter(''); + this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = ''; + } + + selectBranch(index) { + const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index]; + + if (!branch.classList.contains('is-active')) { + branch.click(); + } else { + this.closeDropdown(); + } + } + + closeDropdown() { + this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click(); + } + + get branchInput() { + return this.form.querySelector(`input[name="${this.fieldName}"]`); + } + + get glDropdown() { + return this.$dropdown.data('glDropdown'); + } +} + +window.gl = window.gl || {}; +gl.TargetBranchDropDown = TargetBranchDropDown; diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js index 7e03ec3b391..7e03ec3b391 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js new file mode 100644 index 00000000000..3874c2819a5 --- /dev/null +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -0,0 +1,167 @@ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +/* global Vue */ +/* global BoardService */ + +import FilteredSearchBoards from './filtered_search_boards'; +import eventHub from './eventhub'; + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('./models/issue'); +require('./models/label'); +require('./models/list'); +require('./models/milestone'); +require('./models/user'); +require('./stores/boards_store'); +require('./stores/modal_store'); +require('./services/board_service'); +require('./mixins/modal_mixins'); +require('./mixins/sortable_default_options'); +require('./filters/due_date_filters'); +require('./components/board'); +require('./components/board_sidebar'); +require('./components/new_list_dropdown'); +require('./components/modal/index'); +require('../vue_shared/vue_resource_interceptor'); + +$(() => { + const $boardApp = document.getElementById('board-app'); + const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; + + window.gl = window.gl || {}; + + if (gl.IssueBoardsApp) { + gl.IssueBoardsApp.$destroy(true); + } + + Store.create(); + + gl.IssueBoardsApp = new Vue({ + el: $boardApp, + components: { + 'board': gl.issueBoards.Board, + 'board-sidebar': gl.issueBoards.BoardSidebar, + 'board-add-issues-modal': gl.issueBoards.IssuesModal, + }, + data: { + state: Store.state, + loading: true, + endpoint: $boardApp.dataset.endpoint, + boardId: $boardApp.dataset.boardId, + disabled: $boardApp.dataset.disabled === 'true', + issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, + detailIssue: Store.detail + }, + computed: { + detailIssueVisible () { + return Object.keys(this.detailIssue.issue).length; + }, + }, + created () { + gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + + this.filterManager = new FilteredSearchBoards(Store.filter, true); + + // Listen for updateTokens event + eventHub.$on('updateTokens', this.updateTokens); + }, + beforeDestroy() { + eventHub.$off('updateTokens', this.updateTokens); + }, + mounted () { + Store.disabled = this.disabled; + gl.boardService.all() + .then((resp) => { + resp.json().forEach((board) => { + const list = Store.addList(board); + + if (list.type === 'done') { + list.position = Infinity; + } + }); + + this.state.lists = _.sortBy(this.state.lists, 'position'); + + Store.addBlankState(); + this.loading = false; + }); + }, + methods: { + updateTokens() { + this.filterManager.updateTokens(); + } + }, + }); + + gl.IssueBoardsSearch = new Vue({ + el: document.getElementById('js-add-list'), + data: { + filters: Store.state.filters + }, + mounted () { + gl.issueBoards.newListDropdownInit(); + } + }); + + gl.IssueBoardsModalAddBtn = new Vue({ + mixins: [gl.issueBoards.ModalMixins], + el: document.getElementById('js-add-issues-btn'), + data: { + modal: ModalStore.store, + store: Store.state, + }, + watch: { + disabled() { + this.updateTooltip(); + }, + }, + computed: { + disabled() { + return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; + }, + tooltipTitle() { + if (this.disabled) { + return 'Please add a list to your board first'; + } + + return ''; + }, + }, + methods: { + updateTooltip() { + const $tooltip = $(this.$el); + + this.$nextTick(() => { + if (this.disabled) { + $tooltip.tooltip(); + } else { + $tooltip.tooltip('destroy'); + } + }); + }, + openModal() { + if (!this.disabled) { + this.toggleModal(true); + } + }, + }, + mounted() { + this.updateTooltip(); + }, + template: ` + <button + class="btn btn-create pull-right prepend-left-10" + type="button" + data-placement="bottom" + :class="{ 'disabled': disabled }" + :title="tooltipTitle" + :aria-disabled="disabled" + @click="openModal"> + Add issues + </button> + `, + }); +}); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 deleted file mode 100644 index 878ad1b6031..00000000000 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ -/* global Vue */ -/* global BoardService */ - -function requireAll(context) { return context.keys().map(context); } - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/)); -require('./components/board'); -require('./components/board_sidebar'); -require('./components/new_list_dropdown'); -require('./components/modal/index'); -require('../vue_shared/vue_resource_interceptor'); - -$(() => { - const $boardApp = document.getElementById('board-app'); - const Store = gl.issueBoards.BoardsStore; - const ModalStore = gl.issueBoards.ModalStore; - - window.gl = window.gl || {}; - - if (gl.IssueBoardsApp) { - gl.IssueBoardsApp.$destroy(true); - } - - Store.create(); - - gl.IssueBoardsApp = new Vue({ - el: $boardApp, - components: { - 'board': gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar, - 'board-add-issues-modal': gl.issueBoards.IssuesModal, - }, - data: { - state: Store.state, - loading: true, - endpoint: $boardApp.dataset.endpoint, - boardId: $boardApp.dataset.boardId, - disabled: $boardApp.dataset.disabled === 'true', - issueLinkBase: $boardApp.dataset.issueLinkBase, - rootPath: $boardApp.dataset.rootPath, - bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, - detailIssue: Store.detail - }, - computed: { - detailIssueVisible () { - return Object.keys(this.detailIssue.issue).length; - }, - }, - created () { - gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - }, - mounted () { - Store.disabled = this.disabled; - gl.boardService.all() - .then((resp) => { - resp.json().forEach((board) => { - const list = Store.addList(board); - - if (list.type === 'done') { - list.position = Infinity; - } - }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); - - Store.addBlankState(); - this.loading = false; - }); - } - }); - - gl.IssueBoardsSearch = new Vue({ - el: document.getElementById('js-boards-search'), - data: { - filters: Store.state.filters - }, - mounted () { - gl.issueBoards.newListDropdownInit(); - } - }); - - gl.IssueBoardsModalAddBtn = new Vue({ - mixins: [gl.issueBoards.ModalMixins], - el: document.getElementById('js-add-issues-btn'), - data: { - modal: ModalStore.store, - store: Store.state, - }, - computed: { - disabled() { - return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; - }, - }, - template: ` - <button - class="btn btn-create pull-right prepend-left-10 has-tooltip" - type="button" - :disabled="disabled" - @click="toggleModal(true)"> - Add issues - </button> - `, - }); -}); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js new file mode 100644 index 00000000000..67c0c419713 --- /dev/null +++ b/app/assets/javascripts/boards/components/board.js @@ -0,0 +1,106 @@ +/* eslint-disable comma-dangle, space-before-function-paren, one-var */ +/* global Vue */ +/* global Sortable */ + +import boardBlankState from './board_blank_state'; + +require('./board_delete'); +require('./board_list'); + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.Board = Vue.extend({ + template: '#js-board-template', + components: { + 'board-list': gl.issueBoards.BoardList, + 'board-delete': gl.issueBoards.BoardDelete, + boardBlankState, + }, + props: { + list: Object, + disabled: Boolean, + issueLinkBase: String, + rootPath: String, + }, + data () { + return { + detailIssue: Store.detail, + filter: Store.filter, + }; + }, + watch: { + filter: { + handler() { + this.list.page = 1; + this.list.getIssues(true); + }, + deep: true, + }, + detailIssue: { + handler () { + if (!Object.keys(this.detailIssue.issue).length) return; + + const issue = this.list.findIssue(this.detailIssue.issue.id); + + if (issue) { + const offsetLeft = this.$el.offsetLeft; + const boardsList = document.querySelectorAll('.boards-list')[0]; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); + + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } + + if (right - boardsList.scrollLeft > 0) { + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); + } else if (left > 0) { + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); + } + } + }, + deep: true + } + }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + } + }, + mounted () { + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ + disabled: this.disabled, + group: 'boards', + draggable: '.is-draggable', + handle: '.js-board-handle', + onEnd: (e) => { + gl.issueBoards.onEnd(); + + if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { + const order = this.sortable.toArray(); + const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); + + this.$nextTick(() => { + Store.moveList(list, order); + }); + } + } + }); + + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); + }, + }); +})(); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 deleted file mode 100644 index 18324de18b3..00000000000 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Vue */ -/* global Sortable */ - -require('./board_blank_state'); -require('./board_delete'); -require('./board_list'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', - components: { - 'board-list': gl.issueBoards.BoardList, - 'board-delete': gl.issueBoards.BoardDelete, - 'board-blank-state': gl.issueBoards.BoardBlankState - }, - props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, - }, - data () { - return { - detailIssue: Store.detail, - filters: Store.state.filters, - }; - }, - watch: { - filters: { - handler () { - this.list.page = 1; - this.list.getIssues(true); - }, - deep: true - }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; - - const issue = this.list.findIssue(this.detailIssue.issue.id); - - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); - - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - right -= boardsList.offsetWidth; - } - - if (right - boardsList.scrollLeft > 0) { - $(boardsList).animate({ - scrollLeft: right - }, this.sortableOptions.animation); - } else if (left > 0) { - $(boardsList).animate({ - scrollLeft: offsetLeft - }, this.sortableOptions.animation); - } - } - }, - deep: true - } - }, - methods: { - showNewIssueForm() { - this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - } - }, - mounted () { - this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ - disabled: this.disabled, - group: 'boards', - draggable: '.is-draggable', - handle: '.js-board-handle', - onEnd: (e) => { - gl.issueBoards.onEnd(); - - if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = this.sortable.toArray(); - const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); - - this.$nextTick(() => { - Store.moveList(list, order); - }); - } - } - }); - - this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); - }, - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js new file mode 100644 index 00000000000..52893d4642b --- /dev/null +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -0,0 +1,84 @@ +/* global ListLabel */ +/* global Cookies */ +const Store = gl.issueBoards.BoardsStore; + +export default { + template: ` + <div class="board-blank-state"> + <p> + Add the following default lists to your Issue Board with one click: + </p> + <ul class="board-blank-state-list"> + <li v-for="label in predefinedLabels"> + <span + class="label-color" + :style="{ backgroundColor: label.color }"> + </span> + {{ label.title }} + </li> + </ul> + <p> + Starting out with the default set of lists will get you right on the way to making the most of your board. + </p> + <button + class="btn btn-create btn-inverted btn-block" + type="button" + @click.stop="addDefaultLists"> + Add default lists + </button> + <button + class="btn btn-default btn-block" + type="button" + @click.stop="clearBlankState"> + Nevermind, I'll use my own + </button> + </div> + `, + data() { + return { + predefinedLabels: [ + new ListLabel({ title: 'To Do', color: '#F0AD4E' }), + new ListLabel({ title: 'Doing', color: '#5CB85C' }), + ], + }; + }, + methods: { + addDefaultLists() { + this.clearBlankState(); + + this.predefinedLabels.forEach((label, i) => { + Store.addList({ + title: label.title, + position: i, + list_type: 'label', + label: { + title: label.title, + color: label.color, + }, + }); + }); + + Store.state.lists = _.sortBy(Store.state.lists, 'position'); + + // Save the labels + gl.boardService.generateDefaultLists() + .then((resp) => { + resp.json().forEach((listObj) => { + const list = Store.findList('title', listObj.title); + + list.id = listObj.id; + list.label.id = listObj.label.id; + list.getIssues(); + }); + }) + .catch(() => { + Store.removeList(undefined, 'label'); + Cookies.remove('issue_board_welcome_hidden', { + path: '', + }); + Store.addBlankState(); + }); + }, + clearBlankState: Store.removeBlankState.bind(Store), + }, +}; diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 deleted file mode 100644 index d76314c1892..00000000000 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable space-before-function-paren, comma-dangle */ -/* global Vue */ -/* global ListLabel */ - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardBlankState = Vue.extend({ - data () { - return { - predefinedLabels: [ - new ListLabel({ title: 'To Do', color: '#F0AD4E' }), - new ListLabel({ title: 'Doing', color: '#5CB85C' }) - ] - }; - }, - methods: { - addDefaultLists () { - this.clearBlankState(); - - this.predefinedLabels.forEach((label, i) => { - Store.addList({ - title: label.title, - position: i, - list_type: 'label', - label: { - title: label.title, - color: label.color - } - }); - }); - - Store.state.lists = _.sortBy(Store.state.lists, 'position'); - - // Save the labels - gl.boardService.generateDefaultLists() - .then((resp) => { - resp.json().forEach((listObj) => { - const list = Store.findList('title', listObj.title); - - list.id = listObj.id; - list.label.id = listObj.label.id; - list.getIssues(); - }); - }); - }, - clearBlankState: Store.removeBlankState.bind(Store) - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js new file mode 100644 index 00000000000..4b72090df31 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card.js @@ -0,0 +1,70 @@ +/* global Vue */ +require('./issue_card_inner'); + +const Store = gl.issueBoards.BoardsStore; + +export default { + name: 'BoardsIssueCard', + template: ` + <li class="card" + :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + :index="index" + :data-issue-id="issue.id" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="showIssue($event)"> + <issue-card-inner + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :update-filters="true" /> + </li> + `, + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + props: { + list: Object, + issue: Object, + issueLinkBase: String, + disabled: Boolean, + index: Number, + rootPath: String, + }, + data() { + return { + showDetail: false, + detailIssue: Store.detail, + }; + }, + computed: { + issueDetailVisible() { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + }, + }, + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { + this.showDetail = false; + }, + showIssue(e) { + const targetTagName = e.target.tagName.toLowerCase(); + + if (targetTagName === 'a' || targetTagName === 'button') return; + + if (this.showDetail) { + this.showDetail = false; + + if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + Store.detail.issue = {}; + } else { + Store.detail.issue = this.issue; + Store.detail.list = this.list; + } + } + }, + }, +}; diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 deleted file mode 100644 index 0ea66bd027c..00000000000 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ -/* global Vue */ - -require('./issue_card_inner'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardCard = Vue.extend({ - template: '#js-board-list-card', - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, - props: { - list: Object, - issue: Object, - issueLinkBase: String, - disabled: Boolean, - index: Number, - rootPath: String, - }, - data () { - return { - showDetail: false, - detailIssue: Store.detail - }; - }, - computed: { - issueDetailVisible () { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - } - }, - methods: { - mouseDown () { - this.showDetail = true; - }, - mouseMove() { - this.showDetail = false; - }, - showIssue (e) { - const targetTagName = e.target.tagName.toLowerCase(); - - if (targetTagName === 'a' || targetTagName === 'button') return; - - if (this.showDetail) { - this.showDetail = false; - - if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - Store.detail.issue = {}; - } else { - Store.detail.issue = this.issue; - Store.detail.list = this.list; - } - } - } - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js index 861600424a5..861600424a5 100644 --- a/app/assets/javascripts/boards/components/board_delete.js.es6 +++ b/app/assets/javascripts/boards/components/board_delete.js diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js new file mode 100644 index 00000000000..1330d4ae840 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_list.js @@ -0,0 +1,131 @@ +/* eslint-disable comma-dangle, space-before-function-paren, max-len */ +/* global Vue */ +/* global Sortable */ + +import boardNewIssue from './board_new_issue'; +import boardCard from './board_card'; + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardList = Vue.extend({ + template: '#js-board-list-template', + components: { + boardCard, + boardNewIssue, + }, + props: { + disabled: Boolean, + list: Object, + issues: Array, + loading: Boolean, + issueLinkBase: String, + rootPath: String, + }, + data () { + return { + scrollOffset: 250, + filters: Store.state.filters, + showCount: false, + showIssueForm: false + }; + }, + watch: { + filters: { + handler () { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; + }, + deep: true + }, + issues () { + this.$nextTick(() => { + if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { + this.list.page += 1; + this.list.getIssues(false); + } + + if (this.scrollHeight() > this.listHeight()) { + this.showCount = true; + } else { + this.showCount = false; + } + }); + } + }, + methods: { + listHeight () { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight () { + return this.$refs.list.scrollHeight; + }, + scrollTop () { + return this.$refs.list.scrollTop + this.listHeight(); + }, + loadNextPage () { + const getIssues = this.list.nextPage(); + + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(() => { + this.list.loadingMore = false; + }); + } + }, + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + }, + created() { + gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + }, + mounted () { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], + group: 'issues', + disabled: this.disabled, + filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; + + card.showDetail = false; + Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); + + gl.issueBoards.onStart(); + }, + onAdd: (e) => { + gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + + this.$nextTick(() => { + e.item.remove(); + }); + }, + onUpdate: (e) => { + const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); + }, + onMove(e) { + return !e.related.classList.contains('board-list-count'); + } + }); + + this.sortable = Sortable.create(this.$refs.list, options); + + // Scroll event on list to load more + this.$refs.list.onscroll = () => { + if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + this.loadNextPage(); + } + }; + }, + beforeDestroy() { + gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); + }, + }); +})(); diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 deleted file mode 100644 index 60b0a30af3f..00000000000 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, max-len */ -/* global Vue */ -/* global Sortable */ - -require('./board_card'); -require('./board_new_issue'); - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardList = Vue.extend({ - template: '#js-board-list-template', - components: { - 'board-card': gl.issueBoards.BoardCard, - 'board-new-issue': gl.issueBoards.BoardNewIssue - }, - props: { - disabled: Boolean, - list: Object, - issues: Array, - loading: Boolean, - issueLinkBase: String, - rootPath: String, - }, - data () { - return { - scrollOffset: 250, - filters: Store.state.filters, - showCount: false, - showIssueForm: false - }; - }, - watch: { - filters: { - handler () { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true - }, - issues () { - this.$nextTick(() => { - if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page += 1; - this.list.getIssues(false); - } - - if (this.scrollHeight() > this.listHeight()) { - this.showCount = true; - } else { - this.showCount = false; - } - }); - } - }, - computed: { - orderedIssues () { - return _.sortBy(this.issues, 'priority'); - }, - }, - methods: { - listHeight () { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight () { - return this.$refs.list.scrollHeight; - }, - scrollTop () { - return this.$refs.list.scrollTop + this.listHeight(); - }, - loadNextPage () { - const getIssues = this.list.nextPage(); - - if (getIssues) { - this.list.loadingMore = true; - getIssues.then(() => { - this.list.loadingMore = false; - }); - } - }, - }, - mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ - scroll: document.querySelectorAll('.boards-list')[0], - group: 'issues', - sort: false, - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - onStart: (e) => { - const card = this.$refs.issue[e.oldIndex]; - - card.showDetail = false; - Store.moving.list = card.list; - Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); - - gl.issueBoards.onStart(); - }, - onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); - - this.$nextTick(() => { - e.item.remove(); - }); - }, - }); - - this.sortable = Sortable.create(this.$refs.list, options); - - // Scroll event on list to load more - this.$refs.list.onscroll = () => { - if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { - this.loadNextPage(); - } - }; - } - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js new file mode 100644 index 00000000000..b88f59dd6d4 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -0,0 +1,92 @@ +/* global ListIssue */ +const Store = gl.issueBoards.BoardsStore; + +export default { + name: 'BoardNewIssue', + props: { + list: Object, + }, + data() { + return { + title: '', + error: false, + }; + }, + methods: { + submit(e) { + e.preventDefault(); + if (this.title.trim() === '') return; + + this.error = false; + + const labels = this.list.label ? [this.list.label] : []; + const issue = new ListIssue({ + title: this.title, + labels, + subscribed: true, + }); + + this.list.newIssue(issue) + .then(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + Store.detail.issue = issue; + Store.detail.list = this.list; + }) + .catch(() => { + // Need this because our jQuery very kindly disables buttons on ALL form submissions + $(this.$refs.submitButton).enable(); + + // Remove the issue + this.list.removeIssue(issue); + + // Show error message + this.error = true; + }); + + this.cancel(); + }, + cancel() { + this.title = ''; + gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); + }, + }, + mounted() { + this.$refs.input.focus(); + }, + template: ` + <div class="card board-new-issue-form"> + <form @submit="submit($event)"> + <div class="flash-container" + v-if="error"> + <div class="flash-alert"> + An error occured. Please try again. + </div> + </div> + <label class="label-light" + :for="list.id + '-title'"> + Title + </label> + <input class="form-control" + type="text" + v-model="title" + ref="input" + :id="list.id + '-title'" /> + <div class="clearfix prepend-top-10"> + <button class="btn btn-success pull-left" + type="submit" + :disabled="title === ''" + ref="submit-button"> + Submit issue + </button> + <button class="btn btn-default pull-right" + type="button" + @click="cancel"> + Cancel + </button> + </div> + </form> + </div> + `, +}; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 deleted file mode 100644 index b5c14a198ba..00000000000 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable comma-dangle, no-unused-vars */ -/* global Vue */ -/* global ListIssue */ - -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - - gl.issueBoards.BoardNewIssue = Vue.extend({ - props: { - list: Object, - }, - data() { - return { - title: '', - error: false - }; - }, - methods: { - submit(e) { - e.preventDefault(); - if (this.title.trim() === '') return; - - this.error = false; - - const labels = this.list.label ? [this.list.label] : []; - const issue = new ListIssue({ - title: this.title, - labels, - subscribed: true - }); - - this.list.newIssue(issue) - .then((data) => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - Store.detail.issue = issue; - Store.detail.list = this.list; - }) - .catch(() => { - // Need this because our jQuery very kindly disables buttons on ALL form submissions - $(this.$refs.submitButton).enable(); - - // Remove the issue - this.list.removeIssue(issue); - - // Show error message - this.error = true; - }); - - this.cancel(); - }, - cancel() { - this.title = ''; - this.$parent.showIssueForm = false; - } - }, - mounted() { - this.$refs.input.focus(); - }, - }); -})(); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js index dfc6eed785c..dfc6eed785c 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js new file mode 100644 index 00000000000..69e30cec4c5 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -0,0 +1,114 @@ +/* global Vue */ +import eventHub from '../eventhub'; + +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssueCardInner = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + }, + rootPath: { + type: String, + required: true, + }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + showLabel(label) { + if (!this.list) return true; + + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + if (!this.updateFilters) return; + + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); + $(e.currentTarget).tooltip('hide'); + + if (labelIndex === -1) { + filterPath.push(param); + } else { + filterPath.splice(labelIndex, 1); + } + + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); + + Store.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + template: ` + <div> + <h4 class="card-title"> + <i + class="fa fa-eye-slash confidential-icon" + v-if="issue.confidential"></i> + <a + :href="issueLinkBase + '/' + issue.id" + :title="issue.title"> + {{ issue.title }} + </a> + </h4> + <div class="card-footer"> + <span + class="card-number" + v-if="issue.id"> + #{{ issue.id }} + </span> + <a + class="card-assignee has-tooltip" + :href="rootPath + issue.assignee.username" + :title="'Assigned to ' + issue.assignee.name" + v-if="issue.assignee" + data-container="body"> + <img + class="avatar avatar-inline s20" + :src="issue.assignee.avatar" + width="20" + height="20" + :alt="'Avatar for ' + issue.assignee.name" /> + </a> + <button + class="label color-label has-tooltip" + v-for="label in issue.labels" + type="button" + v-if="showLabel(label)" + @click="filterByLabel(label, $event)" + :style="labelStyle(label)" + :title="label.description" + data-container="body"> + {{ label.title }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 deleted file mode 100644 index 22a8b971ff8..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ /dev/null @@ -1,111 +0,0 @@ -/* global Vue */ -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.IssueCardInner = Vue.extend({ - props: { - issue: { - type: Object, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - list: { - type: Object, - required: false, - }, - rootPath: { - type: String, - required: true, - }, - }, - methods: { - showLabel(label) { - if (!this.list) return true; - - return !this.list.label || label.id !== this.list.label.id; - }, - filterByLabel(label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters.label_name.indexOf(label.title); - $(e.currentTarget).tooltip('hide'); - - if (labelIndex === -1) { - Store.state.filters.label_name.push(label.title); - $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); - } else { - Store.state.filters.label_name.splice(labelIndex, 1); - labelToggleText = Store.state.filters.label_name[0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); - } - - const selectedLabels = Store.state.filters.label_name; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); - - Store.updateFiltersUrl(); - }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, - }, - template: ` - <div> - <h4 class="card-title"> - <i - class="fa fa-eye-slash confidential-icon" - v-if="issue.confidential"></i> - <a - :href="issueLinkBase + '/' + issue.id" - :title="issue.title"> - {{ issue.title }} - </a> - </h4> - <div class="card-footer"> - <span - class="card-number" - v-if="issue.id"> - #{{ issue.id }} - </span> - <a - class="card-assignee has-tooltip" - :href="rootPath + issue.assignee.username" - :title="'Assigned to ' + issue.assignee.name" - v-if="issue.assignee" - data-container="body"> - <img - class="avatar avatar-inline s20" - :src="issue.assignee.avatar" - width="20" - height="20" - :alt="'Avatar for ' + issue.assignee.name" /> - </a> - <button - class="label color-label has-tooltip" - v-for="label in issue.labels" - type="button" - v-if="showLabel(label)" - @click="filterByLabel(label, $event)" - :style="labelStyle(label)" - :title="label.description" - data-container="body"> - {{ label.title }} - </button> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js new file mode 100644 index 00000000000..e6973c3fd59 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -0,0 +1,70 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + props: { + image: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + }, + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; + + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to <strong>Open issues</strong> and select some issues + to add to your board. + `; + } + + return obj; + }, + }, + template: ` + <section class="empty-state"> + <div class="row"> + <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <aside class="svg-content" v-html="image"></aside> + </div> + <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="text-content"> + <h4>{{ contents.title }}</h4> + <p v-html="contents.content"></p> + <a + :href="newIssuePath" + class="btn btn-success btn-inverted" + v-if="activeTab === 'all'"> + New issue + </a> + <button + type="button" + class="btn btn-default" + @click="changeTab('all')" + v-if="activeTab === 'selected'"> + Open issues + </button> + </div> + </div> + </div> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 deleted file mode 100644 index 9538f5b69e9..00000000000 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ /dev/null @@ -1,70 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; - }, - props: { - image: { - type: String, - required: true, - }, - newIssuePath: { - type: String, - required: true, - }, - }, - computed: { - contents() { - const obj = { - title: 'You haven\'t added any issues to your project yet', - content: ` - An issue can be a bug, a todo or a feature request that needs to be - discussed in a project. Besides, issues are searchable and filterable. - `, - }; - - if (this.activeTab === 'selected') { - obj.title = 'You haven\'t selected any issues yet'; - obj.content = ` - Go back to <strong>All issues</strong> and select some issues - to add to your board. - `; - } - - return obj; - }, - }, - template: ` - <section class="empty-state"> - <div class="row"> - <div class="col-xs-12 col-sm-6 col-sm-push-6"> - <aside class="svg-content" v-html="image"></aside> - </div> - <div class="col-xs-12 col-sm-6 col-sm-pull-6"> - <div class="text-content"> - <h4>{{ contents.title }}</h4> - <p v-html="contents.content"></p> - <a - :href="newIssuePath" - class="btn btn-success btn-inverted" - v-if="activeTab === 'all'"> - New issue - </a> - <button - type="button" - class="btn btn-default" - @click="changeTab('all')" - v-if="activeTab === 'selected'"> - All issues - </button> - </div> - </div> - </div> - </section> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js new file mode 100644 index 00000000000..bd394a2318c --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -0,0 +1,24 @@ +import FilteredSearchBoards from '../../filtered_search_boards'; +import FilteredSearchContainer from '../../../filtered_search/container'; + +export default { + name: 'modal-filters', + props: { + store: { + type: Object, + required: true, + }, + }, + mounted() { + FilteredSearchContainer.container = this.$el; + + this.filteredSearch = new FilteredSearchBoards(this.store); + this.filteredSearch.removeTokens(); + }, + beforeDestroy() { + this.filteredSearch.cleanup(); + FilteredSearchContainer.container = document; + this.store.path = ''; + }, + template: '#js-board-modal-filter', +}; diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 deleted file mode 100644 index 6de06811d94..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ /dev/null @@ -1,49 +0,0 @@ -/* global Vue */ -const userFilter = require('./filters/user'); -const milestoneFilter = require('./filters/milestone'); -const labelFilter = require('./filters/label'); - -module.exports = Vue.extend({ - name: 'modal-filters', - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - destroyed() { - gl.issueBoards.ModalStore.setDefaultFilter(); - }, - components: { - userFilter, - milestoneFilter, - labelFilter, - }, - template: ` - <div class="modal-filters"> - <user-filter - dropdown-class-name="dropdown-menu-author" - toggle-class-name="js-user-search js-author-search" - toggle-label="Author" - field-name="author_id" - :project-id="projectId"></user-filter> - <user-filter - dropdown-class-name="dropdown-menu-author" - toggle-class-name="js-assignee-search" - toggle-label="Assignee" - field-name="assignee_id" - :null-user="true" - :project-id="projectId"></user-filter> - <milestone-filter :milestone-path="milestonePath"></milestone-filter> - <label-filter :label-path="labelPath"></label-filter> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 deleted file mode 100644 index 4fc8f72a145..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global LabelsSelect */ -module.exports = Vue.extend({ - name: 'filter-label', - props: { - labelPath: { - type: String, - required: true, - }, - }, - mounted() { - new LabelsSelect(this.$refs.dropdown); - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" - type="button" - data-toggle="dropdown" - data-show-any="true" - data-show-no="true" - :data-labels="labelPath" - ref="dropdown"> - <span class="dropdown-toggle-text"> - Label - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> - <div class="dropdown-title"> - Filter by label - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 deleted file mode 100644 index d555599d300..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global MilestoneSelect */ -module.exports = Vue.extend({ - name: 'filter-milestone', - props: { - milestonePath: { - type: String, - required: true, - }, - }, - mounted() { - new MilestoneSelect(null, this.$refs.dropdown); - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-milestone-select" - type="button" - data-toggle="dropdown" - data-show-any="true" - data-show-upcoming="true" - data-field-name="milestone_title" - :data-milestones="milestonePath" - ref="dropdown"> - <span class="dropdown-toggle-text"> - Milestone - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> - <div class="dropdown-title"> - <span>Filter by milestone</span> - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search milestones" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 deleted file mode 100644 index 8523028c29c..00000000000 --- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue */ -/* global UsersSelect */ -module.exports = Vue.extend({ - name: 'filter-user', - props: { - toggleClassName: { - type: String, - required: true, - }, - dropdownClassName: { - type: String, - required: false, - default: '', - }, - toggleLabel: { - type: String, - required: true, - }, - fieldName: { - type: String, - required: true, - }, - nullUser: { - type: Boolean, - required: false, - default: false, - }, - projectId: { - type: Number, - required: true, - }, - }, - mounted() { - new UsersSelect(null, this.$refs.dropdown); - }, - computed: { - currentUsername() { - return gon.current_username; - }, - dropdownTitle() { - return `Filter by ${this.toggleLabel.toLowerCase()}`; - }, - inputPlaceholder() { - return `Search ${this.toggleLabel.toLowerCase()}`; - }, - }, - template: ` - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-user-search" - :class="toggleClassName" - type="button" - data-toggle="dropdown" - data-current-user="true" - :data-any-user="'Any ' + toggleLabel" - :data-null-user="nullUser" - :data-field-name="fieldName" - :data-project-id="projectId" - :data-first-user="currentUsername" - ref="dropdown"> - <span class="dropdown-toggle-text"> - {{ toggleLabel }} - </span> - <i class="fa fa-chevron-down"></i> - </button> - <div - class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable" - :class="dropdownClassName"> - <div class="dropdown-title"> - {{ dropdownTitle }} - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - autocomplete="off" - :placeholder="inputPlaceholder" /> - <i class="fa fa-search dropdown-input-search"></i> - <i - role="button" - class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> - </i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js index 1cbc422c961..1cbc422c961 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js new file mode 100644 index 00000000000..116e29cd177 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; +import modalFilters from './filters'; + +require('./tabs'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, + }, + template: ` + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)"> + <span aria-hidden="true">×</span> + </button> + </h2> + </header> + <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> + <div + class="add-issues-search append-bottom-10" + v-if="showSearch"> + <modal-filters :store="filter" /> + <button + type="button" + class="btn btn-success btn-inverted prepend-left-10" + ref="selectAllBtn" + @click="toggleAll"> + {{ selectAllText }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 deleted file mode 100644 index 70c088f9054..00000000000 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ /dev/null @@ -1,90 +0,0 @@ -/* global Vue */ -require('./tabs'); -const modalFilters = require('./filters'); - -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - props: { - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } - - return 'Deselect all'; - }, - showSearch() { - return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; - }, - }, - methods: { - toggleAll() { - this.$refs.selectAllBtn.blur(); - - ModalStore.toggleAll(); - }, - }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, - template: ` - <div> - <header class="add-issues-header form-actions"> - <h2> - Add issues - <button - type="button" - class="close" - data-dismiss="modal" - aria-label="Close" - @click="toggleModal(false)"> - <span aria-hidden="true">×</span> - </button> - </h2> - </header> - <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> - <div - class="add-issues-search append-bottom-10" - v-if="showSearch"> - <modal-filters - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath"> - </modal-filters> - <input - placeholder="Search issues..." - class="form-control" - type="search" - v-model="searchTerm" /> - <button - type="button" - class="btn btn-success btn-inverted prepend-left-10" - ref="selectAllBtn" - @click="toggleAll"> - {{ selectAllText }} - </button> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js new file mode 100644 index 00000000000..4240c97617d --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -0,0 +1,159 @@ +/* global Vue */ +/* global ListIssue */ +import queryData from '../../utils/query_data'; + +require('./header'); +require('./list'); +require('./footer'); +require('./empty_state'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.IssuesModal = Vue.extend({ + props: { + blankStateImage: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + page() { + this.loadIssues(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + filter: { + handler() { + this.page = 1; + this.loadIssues(true); + }, + deep: true, + }, + }, + methods: { + loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return false; + + return gl.boardService.getBacklog(queryData(this.filter.path, { + page: this.page, + per: this.perPage, + })).then((res) => { + const data = res.json(); + + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }); + }, + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + created() { + this.page = 1; + }, + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-header> + <modal-list + :image="blankStateImage" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + v-if="!loading && showList"></modal-list> + <empty-state + v-if="showEmptyState" + :image="blankStateImage" + :new-issue-path="newIssuePath"></empty-state> + <section + class="add-issues-list text-center" + v-if="loading"> + <div class="add-issues-list-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + </section> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 deleted file mode 100644 index f290cd13763..00000000000 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ /dev/null @@ -1,163 +0,0 @@ -/* global Vue */ -/* global ListIssue */ - -require('./header'); -require('./list'); -require('./footer'); -require('./empty_state'); - -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.IssuesModal = Vue.extend({ - props: { - blankStateImage: { - type: String, - required: true, - }, - newIssuePath: { - type: String, - required: true, - }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - projectId: { - type: Number, - required: true, - }, - milestonePath: { - type: String, - required: true, - }, - labelPath: { - type: String, - required: true, - }, - }, - data() { - return ModalStore.store; - }, - watch: { - page() { - this.loadIssues(); - }, - searchTerm() { - this.searchOperation(); - }, - showAddIssuesModal() { - if (this.showAddIssuesModal && !this.issues.length) { - this.loading = true; - - this.loadIssues() - .then(() => { - this.loading = false; - }); - } else if (!this.showAddIssuesModal) { - this.issues = []; - this.selectedIssues = []; - this.issuesCount = false; - } - }, - filter: { - handler() { - this.loadIssues(true); - }, - deep: true, - }, - }, - methods: { - searchOperation: _.debounce(function searchOperationDebounce() { - this.loadIssues(true); - }, 500), - loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return false; - - const queryData = Object.assign({}, this.filter, { - search: this.searchTerm, - page: this.page, - per: this.perPage, - }); - - return gl.boardService.getBacklog(queryData).then((res) => { - const data = res.json(); - - if (clearIssues) { - this.issues = []; - } - - data.issues.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; - - this.issues.push(issue); - }); - - this.loadingNewPage = false; - - if (!this.issuesCount) { - this.issuesCount = data.size; - } - }); - }, - }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, - }, - template: ` - <div - class="add-issues-modal" - v-if="showAddIssuesModal"> - <div class="add-issues-container"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath"> - </modal-header> - <modal-list - :image="blankStateImage" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - v-if="!loading && showList"></modal-list> - <empty-state - v-if="showEmptyState" - :image="blankStateImage" - :new-issue-path="newIssuePath"></empty-state> - <section - class="add-issues-list text-center" - v-if="loading"> - <div class="add-issues-list-loading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - </section> - <modal-footer></modal-footer> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js index 3730c1ecaeb..3730c1ecaeb 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 3c05120a2da..3c05120a2da 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js new file mode 100644 index 00000000000..1cd6ca0ee88 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -0,0 +1,47 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + template: ` + <div class="top-area prepend-top-10 append-bottom-10"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')"> + Open issues + <span class="badge"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')"> + Selected issues + <span class="badge"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 deleted file mode 100644 index e8cb43f3503..00000000000 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], - data() { - return ModalStore.store; - }, - computed: { - selectedCount() { - return ModalStore.selectedCount(); - }, - }, - destroyed() { - this.activeTab = 'all'; - }, - template: ` - <div class="top-area prepend-top-10 append-bottom-10"> - <ul class="nav-links issues-state-filters"> - <li :class="{ 'active': activeTab == 'all' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('all')"> - All issues - <span class="badge"> - {{ issuesCount }} - </span> - </a> - </li> - <li :class="{ 'active': activeTab == 'selected' }"> - <a - href="#" - role="button" - @click.prevent="changeTab('selected')"> - Selected issues - <span class="badge"> - {{ selectedCount }} - </span> - </a> - </li> - </ul> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js index 556826a9148..556826a9148 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index e74935e1cb0..e74935e1cb0 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/boards/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js new file mode 100644 index 00000000000..101732309ea --- /dev/null +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ +import FilteredSearchContainer from '../filtered_search/container'; + +export default class FilteredSearchBoards extends gl.FilteredSearchManager { + constructor(store, updateUrl = false) { + super('boards'); + + this.store = store; + this.updateUrl = updateUrl; + + // Issue boards is slightly different, we handle all the requests async + // instead or reloading the page, we just re-fire the list ajax requests + this.isHandledAsync = true; + } + + updateObject(path) { + this.store.path = path.substr(1); + + if (this.updateUrl) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } + } + + removeTokens() { + const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); + + // Remove all the tokens as they will be replaced by the search manager + [].forEach.call(tokens, (el) => { + el.parentNode.removeChild(el); + }); + } + + updateTokens() { + this.removeTokens(); + + this.loadSearchParamsFromURL(); + + // Get the placeholder back if search is empty + this.filteredSearchInput.dispatchEvent(new Event('input')); + } +} diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js index 03425bb145b..03425bb145b 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js index d378b7d4baf..d378b7d4baf 100644 --- a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js index b6c6d17274f..b6c6d17274f 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js new file mode 100644 index 00000000000..ca5e6fa7e9d --- /dev/null +++ b/app/assets/javascripts/boards/models/issue.js @@ -0,0 +1,75 @@ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ +/* global Vue */ +/* global ListLabel */ +/* global ListMilestone */ +/* global ListUser */ + +class ListIssue { + constructor (obj) { + this.globalId = obj.id; + this.id = obj.iid; + this.title = obj.title; + this.confidential = obj.confidential; + this.dueDate = obj.due_date; + this.subscribed = obj.subscribed; + this.labels = []; + this.selected = false; + this.assignee = false; + this.position = obj.relative_position || Infinity; + + if (obj.assignee) { + this.assignee = new ListUser(obj.assignee); + } + + if (obj.milestone) { + this.milestone = new ListMilestone(obj.milestone); + } + + obj.labels.forEach((label) => { + this.labels.push(new ListLabel(label)); + }); + } + + addLabel (label) { + if (!this.findLabel(label)) { + this.labels.push(new ListLabel(label)); + } + } + + findLabel (findLabel) { + return this.labels.filter(label => label.title === findLabel.title)[0]; + } + + removeLabel (removeLabel) { + if (removeLabel) { + this.labels = this.labels.filter(label => removeLabel.title !== label.title); + } + } + + removeLabels (labels) { + labels.forEach(this.removeLabel.bind(this)); + } + + getLists () { + return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); + } + + update (url) { + const data = { + issue: { + milestone_id: this.milestone ? this.milestone.id : null, + due_date: this.dueDate, + assignee_id: this.assignee ? this.assignee.id : null, + label_ids: this.labels.map((label) => label.id) + } + }; + + if (!data.issue.label_ids.length) { + data.issue.label_ids = ['']; + } + + return Vue.http.patch(url, data); + } +} + +window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 deleted file mode 100644 index 2d0a295ae4d..00000000000 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ -/* global Vue */ -/* global ListLabel */ -/* global ListMilestone */ -/* global ListUser */ - -class ListIssue { - constructor (obj) { - this.globalId = obj.id; - this.id = obj.iid; - this.title = obj.title; - this.confidential = obj.confidential; - this.dueDate = obj.due_date; - this.subscribed = obj.subscribed; - this.labels = []; - this.selected = false; - this.assignee = false; - - if (obj.assignee) { - this.assignee = new ListUser(obj.assignee); - } - - if (obj.milestone) { - this.milestone = new ListMilestone(obj.milestone); - } - - obj.labels.forEach((label) => { - this.labels.push(new ListLabel(label)); - }); - - this.priority = this.labels.reduce((max, label) => { - return (label.priority < max) ? label.priority : max; - }, Infinity); - } - - addLabel (label) { - if (!this.findLabel(label)) { - this.labels.push(new ListLabel(label)); - } - } - - findLabel (findLabel) { - return this.labels.filter(label => label.title === findLabel.title)[0]; - } - - removeLabel (removeLabel) { - if (removeLabel) { - this.labels = this.labels.filter(label => removeLabel.title !== label.title); - } - } - - removeLabels (labels) { - labels.forEach(this.removeLabel.bind(this)); - } - - getLists () { - return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); - } - - update (url) { - const data = { - issue: { - milestone_id: this.milestone ? this.milestone.id : null, - due_date: this.dueDate, - assignee_id: this.assignee ? this.assignee.id : null, - label_ids: this.labels.map((label) => label.id) - } - }; - - if (!data.issue.label_ids.length) { - data.issue.label_ids = ['']; - } - - return Vue.http.patch(url, data); - } -} - -window.ListIssue = ListIssue; diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js index 9af88d167d6..9af88d167d6 100644 --- a/app/assets/javascripts/boards/models/label.js.es6 +++ b/app/assets/javascripts/boards/models/label.js diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js new file mode 100644 index 00000000000..f18ad2a0fac --- /dev/null +++ b/app/assets/javascripts/boards/models/list.js @@ -0,0 +1,172 @@ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* global ListIssue */ +/* global ListLabel */ +import queryData from '../utils/query_data'; + +class List { + constructor (obj) { + this.id = obj.id; + this._uid = this.guid(); + this.position = obj.position; + this.title = obj.title; + this.type = obj.list_type; + this.preset = ['done', 'blank'].indexOf(this.type) > -1; + this.page = 1; + this.loading = true; + this.loadingMore = false; + this.issues = []; + this.issuesSize = 0; + + if (obj.label) { + this.label = new ListLabel(obj.label); + } + + if (this.type !== 'blank' && this.id) { + this.getIssues(); + } + } + + guid() { + const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; + } + + save () { + return gl.boardService.createList(this.label.id) + .then((resp) => { + const data = resp.json(); + + this.id = data.id; + this.type = data.list_type; + this.position = data.position; + + return this.getIssues(); + }); + } + + destroy () { + const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); + gl.issueBoards.BoardsStore.state.lists.splice(index, 1); + gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); + + gl.boardService.destroyList(this.id); + } + + update () { + gl.boardService.updateList(this.id, this.position); + } + + nextPage () { + if (this.issuesSize > this.issues.length) { + this.page += 1; + + return this.getIssues(false); + } + } + + getIssues (emptyIssues = true) { + const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); + + if (this.label && data.label_name) { + data.label_name = data.label_name.filter(label => label !== this.label.title); + } + + if (emptyIssues) { + this.loading = true; + } + + return gl.boardService.getIssuesForList(this.id, data) + .then((resp) => { + const data = resp.json(); + this.loading = false; + this.issuesSize = data.size; + + if (emptyIssues) { + this.issues = []; + } + + this.createIssues(data.issues); + }); + } + + newIssue (issue) { + this.addIssue(issue); + this.issuesSize += 1; + + return gl.boardService.newIssue(this.id, issue) + .then((resp) => { + const data = resp.json(); + issue.id = data.iid; + }); + } + + createIssues (data) { + data.forEach((issueObj) => { + this.addIssue(new ListIssue(issueObj)); + }); + } + + addIssue (issue, listFrom, newIndex) { + let moveBeforeIid = null; + let moveAfterIid = null; + + if (!this.findIssue(issue.id)) { + if (newIndex !== undefined) { + this.issues.splice(newIndex, 0, issue); + + if (this.issues[newIndex - 1]) { + moveBeforeIid = this.issues[newIndex - 1].id; + } + + if (this.issues[newIndex + 1]) { + moveAfterIid = this.issues[newIndex + 1].id; + } + } else { + this.issues.push(issue); + } + + if (this.label) { + issue.addLabel(this.label); + } + + if (listFrom) { + this.issuesSize += 1; + + this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid); + } + } + } + + moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { + this.issues.splice(oldIndex, 1); + this.issues.splice(newIndex, 0, issue); + + gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid); + } + + updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) + .then(() => { + listFrom.getIssues(false); + }); + } + + findIssue (id) { + return this.issues.filter(issue => issue.id === id)[0]; + } + + removeIssue (removeIssue) { + this.issues = this.issues.filter((issue) => { + const matchesRemove = removeIssue.id === issue.id; + + if (matchesRemove) { + this.issuesSize -= 1; + issue.removeLabel(this.label); + } + + return !matchesRemove; + }); + } +} + +window.List = List; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 deleted file mode 100644 index 5152be56b66..00000000000 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ -/* global ListIssue */ -/* global ListLabel */ - -class List { - constructor (obj) { - this.id = obj.id; - this._uid = this.guid(); - this.position = obj.position; - this.title = obj.title; - this.type = obj.list_type; - this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filters = gl.issueBoards.BoardsStore.state.filters; - this.page = 1; - this.loading = true; - this.loadingMore = false; - this.issues = []; - this.issuesSize = 0; - - if (obj.label) { - this.label = new ListLabel(obj.label); - } - - if (this.type !== 'blank' && this.id) { - this.getIssues(); - } - } - - guid() { - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); - return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; - } - - save () { - return gl.boardService.createList(this.label.id) - .then((resp) => { - const data = resp.json(); - - this.id = data.id; - this.type = data.list_type; - this.position = data.position; - - return this.getIssues(); - }); - } - - destroy () { - const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); - gl.issueBoards.BoardsStore.state.lists.splice(index, 1); - gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); - - gl.boardService.destroyList(this.id); - } - - update () { - gl.boardService.updateList(this.id, this.position); - } - - nextPage () { - if (this.issuesSize > this.issues.length) { - this.page += 1; - - return this.getIssues(false); - } - } - - getIssues (emptyIssues = true) { - const filters = this.filters; - const data = { page: this.page }; - - Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); - - if (this.label) { - data.label_name = data.label_name.filter(label => label !== this.label.title); - } - - if (emptyIssues) { - this.loading = true; - } - - return gl.boardService.getIssuesForList(this.id, data) - .then((resp) => { - const data = resp.json(); - this.loading = false; - this.issuesSize = data.size; - - if (emptyIssues) { - this.issues = []; - } - - this.createIssues(data.issues); - }); - } - - newIssue (issue) { - this.addIssue(issue); - this.issuesSize += 1; - - return gl.boardService.newIssue(this.id, issue) - .then((resp) => { - const data = resp.json(); - issue.id = data.iid; - }); - } - - createIssues (data) { - data.forEach((issueObj) => { - this.addIssue(new ListIssue(issueObj)); - }); - } - - addIssue (issue, listFrom, newIndex) { - if (!this.findIssue(issue.id)) { - if (newIndex !== undefined) { - this.issues.splice(newIndex, 0, issue); - } else { - this.issues.push(issue); - } - - if (this.label) { - issue.addLabel(this.label); - } - - if (listFrom) { - this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) - .then(() => { - listFrom.getIssues(false); - }); - } - } - } - - findIssue (id) { - return this.issues.filter(issue => issue.id === id)[0]; - } - - removeIssue (removeIssue) { - this.issues = this.issues.filter((issue) => { - const matchesRemove = removeIssue.id === issue.id; - - if (matchesRemove) { - this.issuesSize -= 1; - issue.removeLabel(this.label); - } - - return !matchesRemove; - }); - } -} - -window.List = List; diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js index c867b06d320..c867b06d320 100644 --- a/app/assets/javascripts/boards/models/milestone.js.es6 +++ b/app/assets/javascripts/boards/models/milestone.js diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js index 8e9de4d4cbb..8e9de4d4cbb 100644 --- a/app/assets/javascripts/boards/models/user.js.es6 +++ b/app/assets/javascripts/boards/models/user.js diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js new file mode 100644 index 00000000000..e54102814d6 --- /dev/null +++ b/app/assets/javascripts/boards/services/board_service.js @@ -0,0 +1,97 @@ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ +/* global Vue */ + +class BoardService { + constructor (root, bulkUpdatePath, boardId) { + this.boards = Vue.resource(`${root}{/id}.json`, {}, { + issues: { + method: 'GET', + url: `${root}/${boardId}/issues.json` + } + }); + this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { + generate: { + method: 'POST', + url: `${root}/${boardId}/lists/generate.json` + } + }); + this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + bulkUpdate: { + method: 'POST', + url: bulkUpdatePath, + }, + }); + + Vue.http.interceptors.push((request, next) => { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + next(); + }); + } + + all () { + return this.lists.get(); + } + + generateDefaultLists () { + return this.lists.generate({}); + } + + createList (label_id) { + return this.lists.save({}, { + list: { + label_id + } + }); + } + + updateList (id, position) { + return this.lists.update({ id }, { + list: { + position + } + }); + } + + destroyList (id) { + return this.lists.delete({ id }); + } + + getIssuesForList (id, filter = {}) { + const data = { id }; + Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); + + return this.issues.get(data); + } + + moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) { + return this.issue.update({ id }, { + from_list_id, + to_list_id, + move_before_iid, + move_after_iid, + }); + } + + newIssue (id, issue) { + return this.issues.save({ id }, { + issue + }); + } + + getBacklog(data) { + return this.boards.issues(data); + } + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return this.issues.bulkUpdate(data); + } +} + +window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 deleted file mode 100644 index 065e90518df..00000000000 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ -/* global Vue */ - -class BoardService { - constructor (root, bulkUpdatePath, boardId) { - this.boards = Vue.resource(`${root}{/id}.json`, {}, { - issues: { - method: 'GET', - url: `${root}/${boardId}/issues.json` - } - }); - this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { - generate: { - method: 'POST', - url: `${root}/${boardId}/lists/generate.json` - } - }); - this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { - bulkUpdate: { - method: 'POST', - url: bulkUpdatePath, - }, - }); - - Vue.http.interceptors.push((request, next) => { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - next(); - }); - } - - all () { - return this.lists.get(); - } - - generateDefaultLists () { - return this.lists.generate({}); - } - - createList (label_id) { - return this.lists.save({}, { - list: { - label_id - } - }); - } - - updateList (id, position) { - return this.lists.update({ id }, { - list: { - position - } - }); - } - - destroyList (id) { - return this.lists.delete({ id }); - } - - getIssuesForList (id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); - - return this.issues.get(data); - } - - moveIssue (id, from_list_id, to_list_id) { - return this.issue.update({ id }, { - from_list_id, - to_list_id - }); - } - - newIssue (id, issue) { - return this.issues.save({ id }, { - issue - }); - } - - getBacklog(data) { - return this.boards.issues(data); - } - - bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return this.issues.bulkUpdate(data); - } -} - -window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js new file mode 100644 index 00000000000..28ecb322df7 --- /dev/null +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -0,0 +1,126 @@ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ +/* global Cookies */ +/* global List */ + +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardsStore = { + disabled: false, + filter: { + path: '', + }, + state: {}, + detail: { + issue: {} + }, + moving: { + issue: {}, + list: {} + }, + create () { + this.state.lists = []; + this.filter.path = gl.utils.getUrlParamsArray().join('&'); + }, + addList (listObj) { + const list = new List(listObj); + this.state.lists.push(list); + + return list; + }, + new (listObj) { + const list = this.addList(listObj); + + list + .save() + .then(() => { + this.state.lists = _.sortBy(this.state.lists, 'position'); + }); + this.removeBlankState(); + }, + updateNewListDropdown (listId) { + $(`.js-board-list-${listId}`).removeClass('is-active'); + }, + shouldAddBlankState () { + // Decide whether to add the blank state + return !(this.state.lists.filter(list => list.type !== 'done')[0]); + }, + addBlankState () { + if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; + + this.addList({ + id: 'blank', + list_type: 'blank', + title: 'Welcome to your Issue Board!', + position: 0 + }); + + this.state.lists = _.sortBy(this.state.lists, 'position'); + }, + removeBlankState () { + this.removeList('blank'); + + Cookies.set('issue_board_welcome_hidden', 'true', { + expires: 365 * 10, + path: '' + }); + }, + welcomeIsHidden () { + return Cookies.get('issue_board_welcome_hidden') === 'true'; + }, + removeList (id, type = 'blank') { + const list = this.findList('id', id, type); + + if (!list) return; + + this.state.lists = this.state.lists.filter(list => list.id !== id); + }, + moveList (listFrom, orderLists) { + orderLists.forEach((id, i) => { + const list = this.findList('id', parseInt(id, 10)); + + list.position = i; + }); + listFrom.update(); + }, + moveIssueToList (listFrom, listTo, issue, newIndex) { + const issueTo = listTo.findIssue(issue.id); + const issueLists = issue.getLists(); + const listLabels = issueLists.map(listIssue => listIssue.label); + + if (!issueTo) { + // Add to new lists issues if it doesn't already exist + listTo.addIssue(issue, listFrom, newIndex); + } else { + listTo.updateIssueLabel(issue, listFrom); + issueTo.removeLabel(listFrom.label); + } + + if (listTo.type === 'done') { + issueLists.forEach((list) => { + list.removeIssue(issue); + }); + issue.removeLabels(listLabels); + } else { + listFrom.removeIssue(issue); + } + }, + moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + const beforeId = parseInt(idArray[newIndex - 1], 10) || null; + const afterId = parseInt(idArray[newIndex + 1], 10) || null; + + list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); + }, + findList (key, val, type = 'label') { + return this.state.lists.filter((list) => { + const byType = type ? list['type'] === type : true; + + return list[key] === val && byType; + })[0]; + }, + updateFiltersUrl () { + history.pushState(null, null, `?${this.filter.path}`); + } + }; +})(); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 deleted file mode 100644 index 50842ecbaaa..00000000000 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ -/* global Cookies */ -/* global List */ - -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.BoardsStore = { - disabled: false, - state: {}, - detail: { - issue: {} - }, - moving: { - issue: {}, - list: {} - }, - create () { - this.state.lists = []; - this.state.filters = { - author_id: gl.utils.getParameterValues('author_id')[0], - assignee_id: gl.utils.getParameterValues('assignee_id')[0], - milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]'), - search: '' - }; - }, - addList (listObj) { - const list = new List(listObj); - this.state.lists.push(list); - - return list; - }, - new (listObj) { - const list = this.addList(listObj); - - list - .save() - .then(() => { - this.state.lists = _.sortBy(this.state.lists, 'position'); - }); - this.removeBlankState(); - }, - updateNewListDropdown (listId) { - $(`.js-board-list-${listId}`).removeClass('is-active'); - }, - shouldAddBlankState () { - // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'done')[0]); - }, - addBlankState () { - if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; - - this.addList({ - id: 'blank', - list_type: 'blank', - title: 'Welcome to your Issue Board!', - position: 0 - }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); - }, - removeBlankState () { - this.removeList('blank'); - - Cookies.set('issue_board_welcome_hidden', 'true', { - expires: 365 * 10, - path: '' - }); - }, - welcomeIsHidden () { - return Cookies.get('issue_board_welcome_hidden') === 'true'; - }, - removeList (id, type = 'blank') { - const list = this.findList('id', id, type); - - if (!list) return; - - this.state.lists = this.state.lists.filter(list => list.id !== id); - }, - moveList (listFrom, orderLists) { - orderLists.forEach((id, i) => { - const list = this.findList('id', parseInt(id, 10)); - - list.position = i; - }); - listFrom.update(); - }, - moveIssueToList (listFrom, listTo, issue, newIndex) { - const issueTo = listTo.findIssue(issue.id); - const issueLists = issue.getLists(); - const listLabels = issueLists.map(listIssue => listIssue.label); - - // Add to new lists issues if it doesn't already exist - if (!issueTo) { - listTo.addIssue(issue, listFrom, newIndex); - } - - if (listTo.type === 'done') { - issueLists.forEach((list) => { - list.removeIssue(issue); - }); - issue.removeLabels(listLabels); - } else { - listFrom.removeIssue(issue); - } - }, - findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; - - return list[key] === val && byType; - })[0]; - }, - updateFiltersUrl () { - history.pushState(null, null, `?${$.param(this.state.filters)}`); - } - }; -})(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js new file mode 100644 index 00000000000..7ee266a831f --- /dev/null +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -0,0 +1,99 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + class ModalStore { + constructor() { + this.store = { + columns: 3, + issues: [], + issuesCount: false, + selectedIssues: [], + showAddIssuesModal: false, + activeTab: 'all', + selectedList: null, + searchTerm: '', + loading: false, + loadingNewPage: false, + page: 1, + perPage: 50, + filter: { + path: '', + }, + }; + } + + selectedCount() { + return this.getSelectedIssues().length; + } + + toggleIssue(issueObj) { + const issue = issueObj; + const selected = issue.selected; + + issue.selected = !selected; + + if (!selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + + toggleAll() { + const select = this.selectedCount() !== this.store.issues.length; + + this.store.issues.forEach((issue) => { + const issueUpdate = issue; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + }); + } + + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); + } + + addSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); + + if (index === -1) { + this.store.selectedIssues.push(issue); + } + } + + removeSelectedIssue(issue, forcePurge = false) { + if (this.store.activeTab === 'all' || forcePurge) { + this.store.selectedIssues = this.store.selectedIssues + .filter(fIssue => fIssue.id !== issue.id); + } + } + + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); + } + + selectedIssueIndex(issue) { + return this.store.selectedIssues.indexOf(issue); + } + + findSelectedIssue(issue) { + return this.store.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + } + } + + gl.issueBoards.ModalStore = new ModalStore(); +})(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 deleted file mode 100644 index 15fc6c79e8d..00000000000 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ /dev/null @@ -1,107 +0,0 @@ -(() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - class ModalStore { - constructor() { - this.store = { - columns: 3, - issues: [], - issuesCount: false, - selectedIssues: [], - showAddIssuesModal: false, - activeTab: 'all', - selectedList: null, - searchTerm: '', - loading: false, - loadingNewPage: false, - page: 1, - perPage: 50, - }; - - this.setDefaultFilter(); - } - - setDefaultFilter() { - this.store.filter = { - author_id: '', - assignee_id: '', - milestone_title: '', - label_name: [], - }; - } - - selectedCount() { - return this.getSelectedIssues().length; - } - - toggleIssue(issueObj) { - const issue = issueObj; - const selected = issue.selected; - - issue.selected = !selected; - - if (!selected) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } - } - - toggleAll() { - const select = this.selectedCount() !== this.store.issues.length; - - this.store.issues.forEach((issue) => { - const issueUpdate = issue; - - if (issueUpdate.selected !== select) { - issueUpdate.selected = select; - - if (select) { - this.addSelectedIssue(issue); - } else { - this.removeSelectedIssue(issue); - } - } - }); - } - - getSelectedIssues() { - return this.store.selectedIssues.filter(issue => issue.selected); - } - - addSelectedIssue(issue) { - const index = this.selectedIssueIndex(issue); - - if (index === -1) { - this.store.selectedIssues.push(issue); - } - } - - removeSelectedIssue(issue, forcePurge = false) { - if (this.store.activeTab === 'all' || forcePurge) { - this.store.selectedIssues = this.store.selectedIssues - .filter(fIssue => fIssue.id !== issue.id); - } - } - - purgeUnselectedIssues() { - this.store.selectedIssues.forEach((issue) => { - if (!issue.selected) { - this.removeSelectedIssue(issue, true); - } - }); - } - - selectedIssueIndex(issue) { - return this.store.selectedIssues.indexOf(issue); - } - - findSelectedIssue(issue) { - return this.store.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; - } - } - - gl.issueBoards.ModalStore = new ModalStore(); -})(); diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js new file mode 100644 index 00000000000..2cd3c146f11 --- /dev/null +++ b/app/assets/javascripts/boards/utils/query_data.js @@ -0,0 +1,21 @@ +export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => { + if (filterParam === '') return dataParam; + + const data = dataParam; + const paramSplit = filterParam.split('='); + const paramKeyNormalized = paramSplit[0].replace('[]', ''); + const isArray = paramSplit[0].indexOf('[]'); + const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); + + if (isArray !== -1) { + if (!data[paramKeyNormalized]) { + data[paramKeyNormalized] = []; + } + + data[paramKeyNormalized].push(value); + } else { + data[paramKeyNormalized] = value; + } + + return data; +}, extraData); diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 22e93328548..2c1f988d987 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,72 +1,66 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */ -(function() { - var Breakpoints = (function() { - var BreakpointInstance, instance; +var Breakpoints = (function() { + var BreakpointInstance, instance; - function Breakpoints() {} + function Breakpoints() {} - instance = null; + instance = null; - BreakpointInstance = (function() { - var BREAKPOINTS; + BreakpointInstance = (function() { + var BREAKPOINTS; - BREAKPOINTS = ["xs", "sm", "md", "lg"]; + BREAKPOINTS = ["xs", "sm", "md", "lg"]; - function BreakpointInstance() { - this.setup(); - } - - BreakpointInstance.prototype.setup = function() { - var allDeviceSelector, els; - allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { - return ".device-" + breakpoint; - }); - if ($(allDeviceSelector.join(",")).length) { - return; - } - // Create all the elements - els = $.map(BREAKPOINTS, function(breakpoint) { - return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; - }); - return $("body").append(els.join('')); - }; + function BreakpointInstance() { + this.setup(); + } - BreakpointInstance.prototype.visibleDevice = function() { - var allDeviceSelector; - allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { - return ".device-" + breakpoint; - }); - return $(allDeviceSelector.join(",")).filter(":visible"); - }; - - BreakpointInstance.prototype.getBreakpointSize = function() { - var $visibleDevice; - $visibleDevice = this.visibleDevice; - // TODO: Consider refactoring in light of turbolinks removal. - // the page refreshed via turbolinks - if (!$visibleDevice().length) { - this.setup(); - } - $visibleDevice = this.visibleDevice(); - return $visibleDevice.attr("class").split("visible-")[1]; - }; + BreakpointInstance.prototype.setup = function() { + var allDeviceSelector, els; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + if ($(allDeviceSelector.join(",")).length) { + return; + } + // Create all the elements + els = $.map(BREAKPOINTS, function(breakpoint) { + return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>"; + }); + return $("body").append(els.join('')); + }; - return BreakpointInstance; - })(); + BreakpointInstance.prototype.visibleDevice = function() { + var allDeviceSelector; + allDeviceSelector = BREAKPOINTS.map(function(breakpoint) { + return ".device-" + breakpoint; + }); + return $(allDeviceSelector.join(",")).filter(":visible"); + }; - Breakpoints.get = function() { - return instance != null ? instance : instance = new BreakpointInstance; + BreakpointInstance.prototype.getBreakpointSize = function() { + var $visibleDevice; + $visibleDevice = this.visibleDevice; + // TODO: Consider refactoring in light of turbolinks removal. + // the page refreshed via turbolinks + if (!$visibleDevice().length) { + this.setup(); + } + $visibleDevice = this.visibleDevice(); + return $visibleDevice.attr("class").split("visible-")[1]; }; - return Breakpoints; + return BreakpointInstance; })(); - $((function(_this) { - return function() { - return _this.bp = Breakpoints.get(); - }; - })(this)); + Breakpoints.get = function() { + return instance != null ? instance : instance = new BreakpointInstance; + }; + + return Breakpoints; +})(); + +$(() => { window.bp = Breakpoints.get(); }); - window.Breakpoints = Breakpoints; -}).call(window); +window.Breakpoints = Breakpoints; diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index e8531c43b4b..f73e489e7b2 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,34 +1,33 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */ -(function() { - $(function() { - var previewPath; - $('input#broadcast_message_color').on('input', function() { - var previewColor; - previewColor = $(this).val(); - return $('div.broadcast-message-preview').css('background-color', previewColor); - }); - $('input#broadcast_message_font').on('input', function() { - var previewColor; - previewColor = $(this).val(); - return $('div.broadcast-message-preview').css('color', previewColor); - }); - previewPath = $('textarea#broadcast_message_message').data('preview-path'); - return $('textarea#broadcast_message_message').on('input', function() { - var message; - message = $(this).val(); - if (message === '') { - return $('.js-broadcast-message-preview').text("Your message here"); - } else { - return $.ajax({ - url: previewPath, - type: "POST", - data: { - broadcast_message: { - message: message - } + +$(function() { + var previewPath; + $('input#broadcast_message_color').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('background-color', previewColor); + }); + $('input#broadcast_message_font').on('input', function() { + var previewColor; + previewColor = $(this).val(); + return $('div.broadcast-message-preview').css('color', previewColor); + }); + previewPath = $('textarea#broadcast_message_message').data('preview-path'); + return $('textarea#broadcast_message_message').on('input', function() { + var message; + message = $(this).val(); + if (message === '') { + return $('.js-broadcast-message-preview').text("Your message here"); + } else { + return $.ajax({ + url: previewPath, + type: "POST", + data: { + broadcast_message: { + message: message } - }); - } - }); + } + }); + } }); -}).call(window); +}); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 8fa1aceddff..6efd26ccc37 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,278 +1,283 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ /* global Breakpoints */ -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - var AUTO_SCROLL_OFFSET = 75; - var DOWN_BUILD_TRACE = '#down-build-trace'; - - this.Build = (function() { - Build.interval = null; - - Build.state = null; - - function Build(options) { - options = options || $('.js-build-options').data(); - this.pageUrl = options.pageUrl; - this.buildUrl = options.buildUrl; - this.buildStatus = options.buildStatus; - this.state = options.logState; - this.buildStage = options.buildStage; - this.updateDropdown = bind(this.updateDropdown, this); - this.$document = $(document); - this.$body = $('body'); - this.$buildTrace = $('#build-trace'); - this.$autoScrollContainer = $('.autoscroll-container'); - this.$autoScrollStatus = $('#autoscroll-status'); - this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); - this.$upBuildTrace = $('#up-build-trace'); - this.$downBuildTrace = $(DOWN_BUILD_TRACE); - this.$scrollTopBtn = $('#scroll-top'); - this.$scrollBottomBtn = $('#scroll-bottom'); - this.$buildRefreshAnimation = $('.js-build-refresh'); - - clearInterval(Build.interval); - // Init breakpoint checker - this.bp = Breakpoints.get(); - - this.initSidebar(); - this.$buildScroll = $('#js-build-scroll'); - - this.populateJobs(this.buildStage); - this.updateStageDropdownText(this.buildStage); - this.sidebarOnResize(); - - this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); - this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); - this.$document.on('scroll', this.initScrollMonitor.bind(this)); - $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); - $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); - this.updateArtifactRemoveDate(); - if ($('#build-trace').length) { - this.getInitialBuildTrace(); - this.initScrollButtonAffix(); - } - if (this.buildStatus === "running" || this.buildStatus === "pending") { - Build.interval = setInterval((function(_this) { - // Check for new build output if user still watching build page - // Only valid for runnig build when output changes during time - return function() { - if (_this.location() === _this.pageUrl) { - return _this.getBuildTrace(); - } - }; - })(this), 4000); - } +var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +var AUTO_SCROLL_OFFSET = 75; +var DOWN_BUILD_TRACE = '#down-build-trace'; + +window.Build = (function() { + Build.timeout = null; + + Build.state = null; + + function Build(options) { + options = options || $('.js-build-options').data(); + this.pageUrl = options.pageUrl; + this.buildUrl = options.buildUrl; + this.buildStatus = options.buildStatus; + this.state = options.logState; + this.buildStage = options.buildStage; + this.updateDropdown = bind(this.updateDropdown, this); + this.$document = $(document); + this.$body = $('body'); + this.$buildTrace = $('#build-trace'); + this.$autoScrollContainer = $('.autoscroll-container'); + this.$autoScrollStatus = $('#autoscroll-status'); + this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); + this.$upBuildTrace = $('#up-build-trace'); + this.$downBuildTrace = $(DOWN_BUILD_TRACE); + this.$scrollTopBtn = $('#scroll-top'); + this.$scrollBottomBtn = $('#scroll-bottom'); + this.$buildRefreshAnimation = $('.js-build-refresh'); + + clearTimeout(Build.timeout); + // Init breakpoint checker + this.bp = Breakpoints.get(); + + this.initSidebar(); + this.$buildScroll = $('#js-build-scroll'); + + this.populateJobs(this.buildStage); + this.updateStageDropdownText(this.buildStage); + this.sidebarOnResize(); + + this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); + this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); + this.$document.on('scroll', this.initScrollMonitor.bind(this)); + $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); + $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); + this.updateArtifactRemoveDate(); + if ($('#build-trace').length) { + this.getInitialBuildTrace(); + this.initScrollButtonAffix(); } - - Build.prototype.initSidebar = function() { - this.$sidebar = $('.js-build-sidebar'); - this.$sidebar.niceScroll(); - this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); - }; - - Build.prototype.location = function() { - return window.location.href.split("#")[0]; - }; - - Build.prototype.getInitialBuildTrace = function() { - var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; - - return $.ajax({ - url: this.buildUrl, - dataType: 'json', - success: function(buildData) { - $('.js-build-output').html(buildData.trace_html); - if (window.location.hash === DOWN_BUILD_TRACE) { - $("html,body").scrollTop(this.$buildTrace.height()); + this.invokeBuildTrace(); + } + + Build.prototype.initSidebar = function() { + this.$sidebar = $('.js-build-sidebar'); + this.$sidebar.niceScroll(); + this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); + }; + + Build.prototype.location = function() { + return window.location.href.split("#")[0]; + }; + + Build.prototype.invokeBuildTrace = function() { + var continueRefreshStatuses = ['running', 'pending']; + // Continue to update build trace when build is running or pending + if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) { + // Check for new build output if user still watching build page + // Only valid for runnig build when output changes during time + Build.timeout = setTimeout((function(_this) { + return function() { + if (_this.location() === _this.pageUrl) { + return _this.getBuildTrace(); } - if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { - this.$buildRefreshAnimation.remove(); - return this.initScrollMonitor(); + }; + })(this), 4000); + } + }; + + Build.prototype.getInitialBuildTrace = function() { + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; + + return $.ajax({ + url: this.buildUrl, + dataType: 'json', + success: function(buildData) { + $('.js-build-output').html(buildData.trace_html); + if (window.location.hash === DOWN_BUILD_TRACE) { + $("html,body").scrollTop(this.$buildTrace.height()); + } + if (removeRefreshStatuses.indexOf(buildData.status) !== -1) { + this.$buildRefreshAnimation.remove(); + return this.initScrollMonitor(); + } + }.bind(this) + }); + }; + + Build.prototype.getBuildTrace = function() { + return $.ajax({ + url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)), + dataType: "json", + success: (function(_this) { + return function(log) { + var pageUrl; + + if (log.state) { + _this.state = log.state; } - }.bind(this) - }); - }; - - Build.prototype.getBuildTrace = function() { - return $.ajax({ - url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)), - dataType: "json", - success: (function(_this) { - return function(log) { - var pageUrl; - - if (log.state) { - _this.state = log.state; + _this.invokeBuildTrace(); + if (log.status === "running") { + if (log.append) { + $('.js-build-output').append(log.html); + } else { + $('.js-build-output').html(log.html); } - if (log.status === "running") { - if (log.append) { - $('.js-build-output').append(log.html); - } else { - $('.js-build-output').html(log.html); - } - return _this.checkAutoscroll(); - } else if (log.status !== _this.buildStatus) { - pageUrl = _this.pageUrl; - if (_this.$autoScrollStatus.data('state') === 'enabled') { - pageUrl += DOWN_BUILD_TRACE; - } - - return gl.utils.visitUrl(pageUrl); + return _this.checkAutoscroll(); + } else if (log.status !== _this.buildStatus) { + pageUrl = _this.pageUrl; + if (_this.$autoScrollStatus.data('state') === 'enabled') { + pageUrl += DOWN_BUILD_TRACE; } - }; - })(this) - }); - }; - - Build.prototype.checkAutoscroll = function() { - if (this.$autoScrollStatus.data("state") === "enabled") { - return $("html,body").scrollTop(this.$buildTrace.height()); - } - - // Handle a situation where user started new build - // but never scrolled a page - if (!this.$scrollTopBtn.is(':visible') && - !this.$scrollBottomBtn.is(':visible') && - !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - this.$scrollBottomBtn.show(); - } - }; - Build.prototype.initScrollButtonAffix = function() { - // Hide everything initially - this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.hide(); - this.$autoScrollContainer.hide(); - }; - - // Page scroll listener to detect if user has scrolling page - // and handle following cases - // 1) User is at Top of Build Log; - // - Hide Top Arrow button - // - Show Bottom Arrow button - // - Disable Autoscroll and hide indicator (when build is running) - // 2) User is at Bottom of Build Log; - // - Show Top Arrow button - // - Hide Bottom Arrow button - // - Enable Autoscroll and show indicator (when build is running) - // 3) User is somewhere in middle of Build Log; - // - Show Top Arrow button - // - Show Bottom Arrow button - // - Disable Autoscroll and hide indicator (when build is running) - Build.prototype.initScrollMonitor = function() { - if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // User is somewhere in middle of Build Log - - this.$scrollTopBtn.show(); - - if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed - this.$scrollBottomBtn.show(); - } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { - this.$scrollBottomBtn.show(); - } else { - this.$scrollBottomBtn.hide(); - } - - // Hide Autoscroll Status Indicator - if (this.$scrollBottomBtn.is(':visible')) { - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); - } else { - this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); - this.$autoScrollStatusText.addClass('animate'); - } - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // User is at Top of Build Log + return gl.utils.visitUrl(pageUrl); + } + }; + })(this) + }); + }; + + Build.prototype.checkAutoscroll = function() { + if (this.$autoScrollStatus.data("state") === "enabled") { + return $("html,body").scrollTop(this.$buildTrace.height()); + } - this.$scrollTopBtn.hide(); + // Handle a situation where user started new build + // but never scrolled a page + if (!this.$scrollTopBtn.is(':visible') && + !this.$scrollBottomBtn.is(':visible') && + !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + this.$scrollBottomBtn.show(); + } + }; + + Build.prototype.initScrollButtonAffix = function() { + // Hide everything initially + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + this.$autoScrollContainer.hide(); + }; + + // Page scroll listener to detect if user has scrolling page + // and handle following cases + // 1) User is at Top of Build Log; + // - Hide Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + // 2) User is at Bottom of Build Log; + // - Show Top Arrow button + // - Hide Bottom Arrow button + // - Enable Autoscroll and show indicator (when build is running) + // 3) User is somewhere in middle of Build Log; + // - Show Top Arrow button + // - Show Bottom Arrow button + // - Disable Autoscroll and hide indicator (when build is running) + Build.prototype.initScrollMonitor = function() { + if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is somewhere in middle of Build Log + + this.$scrollTopBtn.show(); + + if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed + this.$scrollBottomBtn.show(); + } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) { this.$scrollBottomBtn.show(); + } else { + this.$scrollBottomBtn.hide(); + } + // Hide Autoscroll Status Indicator + if (this.$scrollBottomBtn.is(':visible')) { this.$autoScrollContainer.hide(); this.$autoScrollStatusText.removeClass('animate'); - } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || - (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { - // User is at Bottom of Build Log - - this.$scrollTopBtn.show(); - this.$scrollBottomBtn.hide(); - - // Show and Reposition Autoscroll Status Indicator + } else { this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); this.$autoScrollStatusText.addClass('animate'); - } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { - // Build Log height is small + } + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // User is at Top of Build Log - this.$scrollTopBtn.hide(); - this.$scrollBottomBtn.hide(); + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.show(); - // Hide Autoscroll Status Indicator - this.$autoScrollContainer.hide(); - this.$autoScrollStatusText.removeClass('animate'); - } + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) || + (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) { + // User is at Bottom of Build Log - if (this.buildStatus === "running" || this.buildStatus === "pending") { - // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. - this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled'); - } - }; - - Build.prototype.shouldHideSidebarForViewport = function() { - var bootstrapBreakpoint; - bootstrapBreakpoint = this.bp.getBreakpointSize(); - return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; - }; - - Build.prototype.toggleSidebar = function(shouldHide) { - var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; - this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) - .toggleClass('sidebar-collapsed', shouldHide); - this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow) - .toggleClass('right-sidebar-collapsed', shouldHide); - }; - - Build.prototype.sidebarOnResize = function() { - this.toggleSidebar(this.shouldHideSidebarForViewport()); - }; - - Build.prototype.sidebarOnClick = function() { - if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); - }; - - Build.prototype.updateArtifactRemoveDate = function() { - var $date, date; - $date = $('.js-artifacts-remove'); - if ($date.length) { - date = $date.text(); - return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); - } - }; - - Build.prototype.populateJobs = function(stage) { - $('.build-job').hide(); - $('.build-job[data-stage="' + stage + '"]').show(); - }; - - Build.prototype.updateStageDropdownText = function(stage) { - $('.stage-selection').text(stage); - }; - - Build.prototype.updateDropdown = function(e) { - e.preventDefault(); - var stage = e.currentTarget.text; - this.updateStageDropdownText(stage); - this.populateJobs(stage); - }; - - Build.prototype.stepTrace = function(e) { - var $currentTarget; - e.preventDefault(); - $currentTarget = $(e.currentTarget); - $.scrollTo($currentTarget.attr('href'), { - offset: 0 - }); - }; - - return Build; - })(); -}).call(window); + this.$scrollTopBtn.show(); + this.$scrollBottomBtn.hide(); + + // Show and Reposition Autoscroll Status Indicator + this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show(); + this.$autoScrollStatusText.addClass('animate'); + } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) { + // Build Log height is small + + this.$scrollTopBtn.hide(); + this.$scrollBottomBtn.hide(); + + // Hide Autoscroll Status Indicator + this.$autoScrollContainer.hide(); + this.$autoScrollStatusText.removeClass('animate'); + } + + if (this.buildStatus === "running" || this.buildStatus === "pending") { + // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise. + this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled'); + } + }; + + Build.prototype.shouldHideSidebarForViewport = function() { + var bootstrapBreakpoint; + bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + }; + + Build.prototype.toggleSidebar = function(shouldHide) { + var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; + this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) + .toggleClass('sidebar-collapsed', shouldHide); + this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow) + .toggleClass('right-sidebar-collapsed', shouldHide); + }; + + Build.prototype.sidebarOnResize = function() { + this.toggleSidebar(this.shouldHideSidebarForViewport()); + }; + + Build.prototype.sidebarOnClick = function() { + if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); + }; + + Build.prototype.updateArtifactRemoveDate = function() { + var $date, date; + $date = $('.js-artifacts-remove'); + if ($date.length) { + date = $date.text(); + return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); + } + }; + + Build.prototype.populateJobs = function(stage) { + $('.build-job').hide(); + $('.build-job[data-stage="' + stage + '"]').show(); + }; + + Build.prototype.updateStageDropdownText = function(stage) { + $('.stage-selection').text(stage); + }; + + Build.prototype.updateDropdown = function(e) { + e.preventDefault(); + var stage = e.currentTarget.text; + this.updateStageDropdownText(stage); + this.populateJobs(stage); + }; + + Build.prototype.stepTrace = function(e) { + var $currentTarget; + e.preventDefault(); + $currentTarget = $(e.currentTarget); + $.scrollTo($currentTarget.attr('href'), { + offset: 0 + }); + }; + + return Build; +})(); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index cae9a0ffca4..bd479700fd3 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,26 +1,25 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ -(function() { - this.BuildArtifacts = (function() { - function BuildArtifacts() { - this.disablePropagation(); - this.setupEntryClick(); - } - BuildArtifacts.prototype.disablePropagation = function() { - $('.top-block').on('click', '.download', function(e) { - return e.stopPropagation(); - }); - return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { - return e.stopImmediatePropagation(); - }); - }; +window.BuildArtifacts = (function() { + function BuildArtifacts() { + this.disablePropagation(); + this.setupEntryClick(); + } - BuildArtifacts.prototype.setupEntryClick = function() { - return $('.tree-holder').on('click', 'tr[data-link]', function(e) { - return window.location = this.dataset.link; - }); - }; + BuildArtifacts.prototype.disablePropagation = function() { + $('.top-block').on('click', '.download', function(e) { + return e.stopPropagation(); + }); + return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { + return e.stopImmediatePropagation(); + }); + }; - return BuildArtifacts; - })(); -}).call(window); + BuildArtifacts.prototype.setupEntryClick = function() { + return $('.tree-holder').on('click', 'tr[data-link]', function(e) { + return window.location = this.dataset.link; + }); + }; + + return BuildArtifacts; +})(); diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js index 99082b412e2..99082b412e2 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js new file mode 100644 index 00000000000..dd4a08a2f31 --- /dev/null +++ b/app/assets/javascripts/ci_lint_editor.js @@ -0,0 +1,17 @@ + +window.gl = window.gl || {}; + +class CILintEditor { + constructor() { + this.editor = window.ace.edit('ci-editor'); + this.textarea = document.querySelector('#content'); + + this.editor.getSession().setMode('ace/mode/yaml'); + this.editor.on('input', () => { + const content = this.editor.getSession().getValue(); + this.textarea.value = content; + }); + } +} + +gl.CILintEditor = CILintEditor; diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6 deleted file mode 100644 index 56ffaa765a8..00000000000 --- a/app/assets/javascripts/ci_lint_editor.js.es6 +++ /dev/null @@ -1,18 +0,0 @@ -(() => { - window.gl = window.gl || {}; - - class CILintEditor { - constructor() { - this.editor = window.ace.edit('ci-editor'); - this.textarea = document.querySelector('#content'); - - this.editor.getSession().setMode('ace/mode/yaml'); - this.editor.on('input', () => { - const content = this.editor.getSession().getValue(); - this.textarea.value = content; - }); - } - } - - gl.CILintEditor = CILintEditor; -})(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 566b322eb49..5f637524e30 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,14 +1,12 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife */ /* global CommitFile */ -(function() { - this.Commit = (function() { - function Commit() { - $('.files .diff-file').each(function() { - return new CommitFile(this); - }); - } +window.Commit = (function() { + function Commit() { + $('.files .diff-file').each(function() { + return new CommitFile(this); + }); + } - return Commit; - })(); -}).call(window); + return Commit; +})(); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 49bb64a3472..17d14dc1e79 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -52,6 +52,30 @@ return this.views[viewMode].call(this); }; + ImageFile.prototype.initDraggable = function($el, padding, callback) { + var dragging = false; + var $body = $('body'); + var $offsetEl = $el.parent(); + + $el.off('mousedown').on('mousedown', function() { + dragging = true; + $body.css('user-select', 'none'); + }); + + $body.off('mouseup').off('mousemove').on('mouseup', function() { + dragging = false; + $body.css('user-select', ''); + }) + .on('mousemove', function(e) { + var left; + if (!dragging) return; + + left = e.pageX - ($offsetEl.offset().left + padding); + + callback(e, left); + }); + }; + prepareFrames = function(view) { var maxHeight, maxWidth; maxWidth = 0; @@ -96,26 +120,30 @@ maxHeight = 0; return $('.swipe.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.swipe-frame', view).css({ + $swipeFrame = $('.swipe-frame', view); + $swipeWrap = $('.swipe-wrap', view); + $swipeBar = $('.swipe-bar', view); + + $swipeFrame.css({ width: maxWidth + 16, height: maxHeight + 28 }); - $('.swipe-wrap', view).css({ + $swipeWrap.css({ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.swipe-bar', view).css({ + $swipeBar.css({ left: 0 - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); - }, - stop: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + }); + + wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); + + _this.initDraggable($swipeBar, wrapPadding, function(e, left) { + if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) { + $swipeWrap.width((maxWidth + 1) - left); + $swipeBar.css('left', left); } }); }; @@ -128,9 +156,14 @@ dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); return $('.onion-skin.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.onion-skin-frame', view).css({ + $frame = $('.onion-skin-frame', view); + $frameAdded = $('.frame.added', view); + $track = $('.drag-track', view); + $dragger = $('.dragger', $track); + + $frame.css({ width: maxWidth + 16, height: maxHeight + 28 }); @@ -138,16 +171,18 @@ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.dragger', view).css({ + $dragger.css({ left: dragTrackWidth - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); - }, - stop: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + }); + + framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); + + _this.initDraggable($dragger, framePadding, function(e, left) { + var opacity = left / dragTrackWidth; + + if (opacity >= 0 && opacity <= 1) { + $dragger.css('left', left); + $frameAdded.css('opacity', opacity); } }); }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js new file mode 100644 index 00000000000..a9f2d462c31 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -0,0 +1,30 @@ +/* eslint-disable no-param-reassign */ +import CommitPipelinesTable from './pipelines_table'; + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); + +/** + * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. + */ + +$(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); + } + + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable(); + + if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { + gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); + } +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 deleted file mode 100644 index b5a988df897..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-new, no-param-reassign */ -/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ - -window.Vue = require('vue'); -require('./pipelines_table'); -/** - * Commits View > Pipelines Tab > Pipelines Table. - * Merge Request View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. - * Renders Pipelines table in pipelines tab in the merge request show view. - */ - -$(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - - if (gl.commits.PipelinesTableBundle) { - gl.commits.PipelinesTableBundle.$destroy(true); - } - - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView(); - - if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); - } -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 deleted file mode 100644 index 8ae98f9bf97..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* globals Vue */ -/* eslint-disable no-unused-vars, no-param-reassign */ - -/** - * Pipelines service. - * - * Used to fetch the data used to render the pipelines table. - * Uses Vue.Resource - */ -class PipelinesService { - - /** - * FIXME: The url provided to request the pipelines in the new merge request - * page already has `.json`. - * This should be fixed when the endpoint is improved. - * - * @param {String} root - */ - constructor(root) { - let endpoint; - - if (root.indexOf('.json') === -1) { - endpoint = `${root}.json`; - } else { - endpoint = root; - } - this.pipelines = Vue.resource(endpoint); - } - - /** - * Given the root param provided when the class is initialized, will - * make a GET request. - * - * @return {Promise} - */ - all() { - return this.pipelines.get(); - } -} - -window.gl = window.gl || {}; -gl.commits = gl.commits || {}; -gl.commits.pipelines = gl.commits.pipelines || {}; -gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 deleted file mode 100644 index f1b80e45444..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable no-underscore-dangle*/ -/** - * Pipelines' Store for commits view. - * - * Used to store the Pipelines rendered in the commit view in the pipelines table. - */ -require('../../vue_realtime_listener'); - -class PipelinesStore { - constructor() { - this.state = {}; - this.state.pipelines = []; - } - - storePipelines(pipelines = []) { - this.state.pipelines = pipelines; - - return pipelines; - } - - /** - * Once the data is received we will start the time ago loops. - * - * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we - * update the time to show how long as passed. - * - */ - static startTimeAgoLoops() { - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - - startTimeLoops(); - - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); - - gl.VueRealtimeListener(removeIntervals, startIntervals); - } -} - -module.exports = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js new file mode 100644 index 00000000000..832c4b1bd2a --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -0,0 +1,110 @@ +/* eslint-disable no-new*/ +/* global Flash */ +import Vue from 'vue'; +import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; +import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; +import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; +import eventHub from '../../vue_pipelines_index/event_hub'; +import '../../lib/utils/common_utils'; +import '../../vue_shared/vue_resource_interceptor'; + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +export default Vue.component('pipelines-table', { + components: { + 'pipelines-table-component': PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const store = new PipelineStore(); + + return { + endpoint: pipelinesTableData.endpoint, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is about to be mounted, tell the service to fetch the data + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + beforeMount() { + this.service = new PipelinesService(this.endpoint); + + this.fetchPipelines(); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + }, + + beforeUpdate() { + if (this.state.pipelines.length && this.$children) { + this.store.startTimeAgoLoops.call(this, Vue); + } + }, + + beforeDestroyed() { + eventHub.$off('refreshPipelines'); + }, + + methods: { + fetchPipelines() { + this.isLoading = true; + return this.service.getPipelines() + .then(response => response.json()) + .then((json) => { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = json.pipelines || json; + this.store.storePipelines(pipelines); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.'); + }); + }, + }, + + template: ` + <div class="pipelines"> + <div class="realtime-loading" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder pipelines" + v-if="!isLoading && state.pipelines.length > 0"> + <pipelines-table-component + :pipelines="state.pipelines" + :service="service" /> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 deleted file mode 100644 index e7c6c063413..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-disable no-new, no-param-reassign */ -/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ - -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('../../lib/utils/common_utils'); -require('../../vue_shared/vue_resource_interceptor'); -require('../../vue_shared/components/pipelines_table'); -require('./pipelines_service'); -const PipelineStore = require('./pipelines_store'); - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - * Necessary SVG in the table are provided as props. This should be refactored - * as soon as we have Webpack and can load them directly into JS files. - */ - -(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - - gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { - - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = new PipelineStore(); - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgsData); - - return { - endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, - store, - state: store.state, - isLoading: false, - }; - }, - - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); - - this.isLoading = true; - return pipelinesService.all() - .then(response => response.json()) - .then((json) => { - this.store.storePipelines(json); - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); - }); - }, - - beforeUpdate() { - if (this.state.pipelines.length && this.$children) { - PipelineStore.startTimeAgoLoops.call(this, Vue); - } - }, - - template: ` - <div class="pipelines"> - <div class="realtime-loading" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> - </div> - - <div class="table-holder pipelines" - v-if="!isLoading && state.pipelines.length > 0"> - <pipelines-table-component - :pipelines="state.pipelines" - :svgs="svgs"> - </pipelines-table-component> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index ccd895f3bf4..e3f9eaaf39c 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,68 +1,66 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ /* global Pager */ -(function() { - this.CommitsList = (function() { - var CommitsList = {}; +window.CommitsList = (function() { + var CommitsList = {}; - CommitsList.timer = null; + CommitsList.timer = null; - CommitsList.init = function(limit) { - $("body").on("click", ".day-commits-table li.commit", function(e) { - if (e.target.nodeName !== "A") { - location.href = $(this).attr("url"); - e.stopPropagation(); - return false; - } - }); - Pager.init(limit, false, false, function() { - gl.utils.localTimeAgo($('.js-timeago')); - }); - this.content = $("#commits-list"); - this.searchField = $("#commits-search"); - this.lastSearch = this.searchField.val(); - return this.initSearch(); - }; + CommitsList.init = function(limit) { + $("body").on("click", ".day-commits-table li.commit", function(e) { + if (e.target.nodeName !== "A") { + location.href = $(this).attr("url"); + e.stopPropagation(); + return false; + } + }); + Pager.init(limit, false, false, function() { + gl.utils.localTimeAgo($('.js-timeago')); + }); + this.content = $("#commits-list"); + this.searchField = $("#commits-search"); + this.lastSearch = this.searchField.val(); + return this.initSearch(); + }; - CommitsList.initSearch = function() { - this.timer = null; - return this.searchField.keyup((function(_this) { - return function() { - clearTimeout(_this.timer); - return _this.timer = setTimeout(_this.filterResults, 500); - }; - })(this)); - }; + CommitsList.initSearch = function() { + this.timer = null; + return this.searchField.keyup((function(_this) { + return function() { + clearTimeout(_this.timer); + return _this.timer = setTimeout(_this.filterResults, 500); + }; + })(this)); + }; - CommitsList.filterResults = function() { - var commitsUrl, form, search; - form = $(".commits-search-form"); - search = CommitsList.searchField.val(); - if (search === CommitsList.lastSearch) return; - commitsUrl = form.attr("action") + '?' + form.serialize(); - CommitsList.content.fadeTo('fast', 0.5); - return $.ajax({ - type: "GET", - url: form.attr("action"), - data: form.serialize(), - complete: function() { - return CommitsList.content.fadeTo('fast', 1.0); - }, - success: function(data) { - CommitsList.lastSearch = search; - CommitsList.content.html(data.html); - return history.replaceState({ - page: commitsUrl - // Change url so if user reload a page - search results are saved - }, document.title, commitsUrl); - }, - error: function() { - CommitsList.lastSearch = null; - }, - dataType: "json" - }); - }; + CommitsList.filterResults = function() { + var commitsUrl, form, search; + form = $(".commits-search-form"); + search = CommitsList.searchField.val(); + if (search === CommitsList.lastSearch) return; + commitsUrl = form.attr("action") + '?' + form.serialize(); + CommitsList.content.fadeTo('fast', 0.5); + return $.ajax({ + type: "GET", + url: form.attr("action"), + data: form.serialize(), + complete: function() { + return CommitsList.content.fadeTo('fast', 1.0); + }, + success: function(data) { + CommitsList.lastSearch = search; + CommitsList.content.html(data.html); + return history.replaceState({ + page: commitsUrl + // Change url so if user reload a page - search results are saved + }, document.title, commitsUrl); + }, + error: function() { + CommitsList.lastSearch = null; + }, + dataType: "json" + }); + }; - return CommitsList; - })(); -}).call(window); + return CommitsList; +})(); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js new file mode 100644 index 00000000000..36bfe457be9 --- /dev/null +++ b/app/assets/javascripts/commons/bootstrap.js @@ -0,0 +1,16 @@ +import $ from 'jquery'; + +// bootstrap jQuery plugins +import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; +import 'bootstrap-sass/assets/javascripts/bootstrap/alert'; +import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'; +import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; +import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; +import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; +import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; + +// custom jQuery functions +$.fn.extend({ + disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); }, + enable() { return $(this).removeAttr('disabled').removeClass('disabled'); }, +}); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js new file mode 100644 index 00000000000..7063f59d446 --- /dev/null +++ b/app/assets/javascripts/commons/index.js @@ -0,0 +1,3 @@ +import './polyfills'; +import './jquery'; +import './bootstrap'; diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js new file mode 100644 index 00000000000..b53f6284afc --- /dev/null +++ b/app/assets/javascripts/commons/jquery.js @@ -0,0 +1,11 @@ +import 'jquery'; + +// common jQuery plugins +import 'jquery-ujs'; +import 'vendor/jquery.endless-scroll'; +import 'vendor/jquery.caret'; +import 'vendor/jquery.atwho'; +import 'vendor/jquery.scrollTo'; +import 'vendor/jquery.nicescroll'; +import 'vendor/jquery.waitforimages'; +import 'select2/select2'; diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js new file mode 100644 index 00000000000..fbd0db64ca7 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills.js @@ -0,0 +1,10 @@ +// ECMAScript polyfills +import 'core-js/fn/array/find'; +import 'core-js/fn/object/assign'; +import 'core-js/fn/promise'; +import 'core-js/fn/string/code-point-at'; +import 'core-js/fn/string/from-code-point'; + +// Browser polyfills +import './polyfills/custom_event'; +import './polyfills/element'; diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js new file mode 100644 index 00000000000..aea61b82d03 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/custom_event.js @@ -0,0 +1,9 @@ +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function CustomEvent(event, params) { + const evt = document.createEvent('CustomEvent'); + const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; + evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); + return evt; + }; + window.CustomEvent.prototype = Event; +} diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js new file mode 100644 index 00000000000..9a1f73bf2ac --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -0,0 +1,20 @@ +Element.prototype.closest = Element.prototype.closest || + function closest(selector, selectedElement = this) { + if (!selectedElement) return null; + return selectedElement.matches(selector) ? + selectedElement : + Element.prototype.closest(selector, selectedElement.parentElement); + }; + +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function matches(selector) { + const elms = (this.document || this.ownerDocument).querySelectorAll(selector); + let i = elms.length - 1; + while (i >= 0 && elms.item(i) !== this) { i -= 1; } + return i > -1; + }; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 15df105d4cc..9e5dbd64a7e 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,91 +1,90 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ -(function() { - this.Compare = (function() { - function Compare(opts) { - this.opts = opts; - this.source_loading = $(".js-source-loading"); - this.target_loading = $(".js-target-loading"); - $('.js-compare-dropdown').each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return $dropdown.glDropdown({ - selectable: true, - fieldName: $dropdown.data('field-name'), - filterable: true, - id: function(obj, $el) { - return $el.data('id'); - }, - toggleLabel: function(obj, $el) { - return $el.text().trim(); - }, - clicked: function(e, el) { - if ($dropdown.is('.js-target-branch')) { - return _this.getTargetHtml(); - } else if ($dropdown.is('.js-source-branch')) { - return _this.getSourceHtml(); - } else if ($dropdown.is('.js-target-project')) { - return _this.getTargetProject(); - } + +window.Compare = (function() { + function Compare(opts) { + this.opts = opts; + this.source_loading = $(".js-source-loading"); + this.target_loading = $(".js-target-loading"); + $('.js-compare-dropdown').each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + selectable: true, + fieldName: $dropdown.data('field-name'), + filterable: true, + id: function(obj, $el) { + return $el.data('id'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e, el) { + if ($dropdown.is('.js-target-branch')) { + return _this.getTargetHtml(); + } else if ($dropdown.is('.js-source-branch')) { + return _this.getSourceHtml(); + } else if ($dropdown.is('.js-target-project')) { + return _this.getTargetProject(); } - }); - }; - })(this)); - this.initialState(); - } + } + }); + }; + })(this)); + this.initialState(); + } - Compare.prototype.initialState = function() { - this.getSourceHtml(); - return this.getTargetHtml(); - }; + Compare.prototype.initialState = function() { + this.getSourceHtml(); + return this.getTargetHtml(); + }; - Compare.prototype.getTargetProject = function() { - return $.ajax({ - url: this.opts.targetProjectUrl, - data: { - target_project_id: $("input[name='merge_request[target_project_id]']").val() - }, - beforeSend: function() { - return $('.mr_target_commit').empty(); - }, - success: function(html) { - return $('.js-target-branch-dropdown .dropdown-content').html(html); - } - }); - }; + Compare.prototype.getTargetProject = function() { + return $.ajax({ + url: this.opts.targetProjectUrl, + data: { + target_project_id: $("input[name='merge_request[target_project_id]']").val() + }, + beforeSend: function() { + return $('.mr_target_commit').empty(); + }, + success: function(html) { + return $('.js-target-branch-dropdown .dropdown-content').html(html); + } + }); + }; - Compare.prototype.getSourceHtml = function() { - return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { - ref: $("input[name='merge_request[source_branch]']").val() - }); - }; + Compare.prototype.getSourceHtml = function() { + return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { + ref: $("input[name='merge_request[source_branch]']").val() + }); + }; - Compare.prototype.getTargetHtml = function() { - return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { - target_project_id: $("input[name='merge_request[target_project_id]']").val(), - ref: $("input[name='merge_request[target_branch]']").val() - }); - }; + Compare.prototype.getTargetHtml = function() { + return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), + ref: $("input[name='merge_request[target_branch]']").val() + }); + }; - Compare.prototype.sendAjax = function(url, loading, target, data) { - var $target; - $target = $(target); - return $.ajax({ - url: url, - data: data, - beforeSend: function() { - loading.show(); - return $target.empty(); - }, - success: function(html) { - loading.hide(); - $target.html(html); - var className = '.' + $target[0].className.replace(' ', '.'); - gl.utils.localTimeAgo($('.js-timeago', className)); - } - }); - }; + Compare.prototype.sendAjax = function(url, loading, target, data) { + var $target; + $target = $(target); + return $.ajax({ + url: url, + data: data, + beforeSend: function() { + loading.show(); + return $target.empty(); + }, + success: function(html) { + loading.hide(); + $target.html(html); + var className = '.' + $target[0].className.replace(' ', '.'); + gl.utils.localTimeAgo($('.js-timeago', className)); + } + }); + }; - return Compare; - })(); -}).call(window); + return Compare; +})(); diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js new file mode 100644 index 00000000000..72c0d98d47c --- /dev/null +++ b/app/assets/javascripts/compare_autocomplete.js @@ -0,0 +1,68 @@ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ + +window.CompareAutocomplete = (function() { + function CompareAutocomplete() { + this.initDropdown(); + } + + CompareAutocomplete.prototype.initDropdown = function() { + return $('.js-compare-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + const $dropdownContainer = $dropdown.closest('.dropdown'); + const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); + const $filterInput = $('input[type="search"]', $dropdownContainer); + $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref'), + search: term, + } + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterRemote: true, + fieldName: $dropdown.data('field-name'), + filterInput: 'input[type="search"]', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); + } + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + } + }); + $filterInput.on('keyup', (e) => { + const keyCode = e.keyCode || e.which; + if (keyCode !== 13) return; + const text = $filterInput.val(); + $fieldInput.val(text); + $('.dropdown-toggle-text', $dropdown).text(text); + $dropdownContainer.removeClass('open'); + }); + + $dropdownContainer.on('click', '.dropdown-content a', (e) => { + $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); + if ($dropdown.hasClass('has-tooltip')) { + $dropdown.tooltip('fixTitle'); + } + }); + }); + }; + + return CompareAutocomplete; +})(); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 deleted file mode 100644 index 1eca973e069..00000000000 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ - -(function() { - this.CompareAutocomplete = (function() { - function CompareAutocomplete() { - this.initDropdown(); - } - - CompareAutocomplete.prototype.initDropdown = function() { - return $('.js-compare-dropdown').each(function() { - var $dropdown, selected; - $dropdown = $(this); - selected = $dropdown.data('selected'); - const $dropdownContainer = $dropdown.closest('.dropdown'); - const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); - const $filterInput = $('input[type="search"]', $dropdownContainer); - $dropdown.glDropdown({ - data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { - ref: $dropdown.data('ref') - } - }).done(function(refs) { - return callback(refs); - }); - }, - selectable: true, - filterable: true, - filterByText: true, - fieldName: $dropdown.data('field-name'), - filterInput: 'input[type="search"]', - renderRow: function(ref) { - var link; - if (ref.header != null) { - return $('<li />').addClass('dropdown-header').text(ref.header); - } else { - link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); - return $('<li />').append(link); - } - }, - id: function(obj, $el) { - return $el.attr('data-ref'); - }, - toggleLabel: function(obj, $el) { - return $el.text().trim(); - } - }); - $filterInput.on('keyup', (e) => { - const keyCode = e.keyCode || e.which; - if (keyCode !== 13) return; - const text = $filterInput.val(); - $fieldInput.val(text); - $('.dropdown-toggle-text', $dropdown).text(text); - $dropdownContainer.removeClass('open'); - }); - - $dropdownContainer.on('click', '.dropdown-content a', (e) => { - $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); - if ($dropdown.hasClass('has-tooltip')) { - $dropdown.tooltip('fixTitle'); - } - }); - }); - }; - - return CompareAutocomplete; - })(); -}).call(window); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index a1c1b721228..b375b61202e 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,31 +1,30 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ -(function() { - this.ConfirmDangerModal = (function() { - function ConfirmDangerModal(form, text) { - var project_path, submit; - this.form = form; - $('.js-confirm-text').text(text || ''); - $('.js-confirm-danger-input').val(''); - $('#modal-confirm-danger').modal('show'); - project_path = $('.js-confirm-danger-match').text(); - submit = $('.js-confirm-danger-submit'); - submit.disable(); - $('.js-confirm-danger-input').off('input'); - $('.js-confirm-danger-input').on('input', function() { - if (gl.utils.rstrip($(this).val()) === project_path) { - return submit.enable(); - } else { - return submit.disable(); - } - }); - $('.js-confirm-danger-submit').off('click'); - $('.js-confirm-danger-submit').on('click', (function(_this) { - return function() { - return _this.form.submit(); - }; - })(this)); - } - return ConfirmDangerModal; - })(); -}).call(window); +window.ConfirmDangerModal = (function() { + function ConfirmDangerModal(form, text) { + var project_path, submit; + this.form = form; + $('.js-confirm-text').text(text || ''); + $('.js-confirm-danger-input').val(''); + $('#modal-confirm-danger').modal('show'); + project_path = $('.js-confirm-danger-match').text(); + submit = $('.js-confirm-danger-submit'); + submit.disable(); + $('.js-confirm-danger-input').off('input'); + $('.js-confirm-danger-input').on('input', function() { + if (gl.utils.rstrip($(this).val()) === project_path) { + return submit.enable(); + } else { + return submit.disable(); + } + }); + $('.js-confirm-danger-submit').off('click'); + $('.js-confirm-danger-submit').on('click', (function(_this) { + return function() { + return _this.form.submit(); + }; + })(this)); + } + + return ConfirmDangerModal; +})(); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js new file mode 100644 index 00000000000..570799c030e --- /dev/null +++ b/app/assets/javascripts/copy_as_gfm.js @@ -0,0 +1,402 @@ +/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ + +require('./lib/utils/common_utils'); + +const gfmRules = { + // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert + // GitLab Flavored Markdown (GFM) to HTML. + // These handlers consequently convert that same HTML to GFM to be copied to the clipboard. + // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML + // from GFM should have a handler here, in reverse order. + // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. + InlineDiffFilter: { + 'span.idiff.addition'(el, text) { + return `{+${text}+}`; + }, + 'span.idiff.deletion'(el, text) { + return `{-${text}-}`; + }, + }, + TaskListFilter: { + 'input[type=checkbox].task-list-item-checkbox'(el, text) { + return `[${el.checked ? 'x' : ' '}]`; + }, + }, + ReferenceFilter: { + '.tooltip'(el, text) { + return ''; + }, + 'a.gfm:not([data-link=true])'(el, text) { + return el.dataset.original || text; + }, + }, + AutolinkFilter: { + 'a'(el, text) { + // Fallback on the regular MarkdownFilter's `a` handler. + if (text !== el.getAttribute('href')) return false; + + return text; + }, + }, + TableOfContentsFilter: { + 'ul.section-nav'(el, text) { + return '[[_TOC_]]'; + }, + }, + EmojiFilter: { + 'img.emoji'(el, text) { + return el.getAttribute('alt'); + }, + 'gl-emoji'(el, text) { + return `:${el.getAttribute('data-name')}:`; + }, + }, + ImageLinkFilter: { + 'a.no-attachment-icon'(el, text) { + return text; + }, + }, + VideoLinkFilter: { + '.video-container'(el, text) { + const videoEl = el.querySelector('video'); + if (!videoEl) return false; + + return CopyAsGFM.nodeToGFM(videoEl); + }, + 'video'(el, text) { + return `![${el.dataset.title}](${el.getAttribute('src')})`; + }, + }, + MathFilter: { + 'pre.code.math[data-math-style=display]'(el, text) { + return `\`\`\`math\n${text.trim()}\n\`\`\``; + }, + 'code.code.math[data-math-style=inline]'(el, text) { + return `$\`${text}\`$`; + }, + 'span.katex-display span.katex-mathml'(el, text) { + const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) return false; + + return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; + }, + 'span.katex-mathml'(el, text) { + const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) return false; + + return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; + }, + 'span.katex-html'(el, text) { + // We don't want to include the content of this element in the copied text. + return ''; + }, + 'annotation[encoding="application/x-tex"]'(el, text) { + return text.trim(); + }, + }, + SanitizationFilter: { + 'a[name]:not([href]):empty'(el, text) { + return el.outerHTML; + }, + 'dl'(el, text) { + let lines = text.trim().split('\n'); + // Add two spaces to the front of subsequent list items lines, + // or leave the line entirely blank. + lines = lines.map((l) => { + const line = l.trim(); + if (line.length === 0) return ''; + + return ` ${line}`; + }); + + return `<dl>\n${lines.join('\n')}\n</dl>`; + }, + 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}</${tag}>`; + }, + }, + SyntaxHighlightFilter: { + 'pre.code.highlight'(el, t) { + const text = t.trimRight(); + + let lang = el.getAttribute('lang'); + if (!lang || lang === 'plaintext') { + lang = ''; + } + + // Prefixes lines with 4 spaces if the code contains triple backticks + if (lang === '' && text.match(/^```/gm)) { + return text.split('\n').map((l) => { + const line = l.trim(); + if (line.length === 0) return ''; + + return ` ${line}`; + }).join('\n'); + } + + return `\`\`\`${lang}\n${text}\n\`\`\``; + }, + 'pre > code'(el, text) { + // Don't wrap code blocks in `` + return text; + }, + }, + MarkdownFilter: { + 'br'(el, text) { + // Two spaces at the end of a line are turned into a BR + return ' '; + }, + 'code'(el, text) { + let backtickCount = 1; + const backtickMatch = text.match(/`+/); + if (backtickMatch) { + backtickCount = backtickMatch[0].length + 1; + } + + const backticks = Array(backtickCount + 1).join('`'); + const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; + + return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; + }, + 'blockquote'(el, text) { + return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); + }, + 'img'(el, text) { + return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + }, + 'a.anchor'(el, text) { + // Don't render a Markdown link for the anchor link inside a heading + return text; + }, + 'a'(el, text) { + return `[${text}](${el.getAttribute('href')})`; + }, + 'li'(el, text) { + const lines = text.trim().split('\n'); + const firstLine = `- ${lines.shift()}`; + // Add four spaces to the front of subsequent list items lines, + // or leave the line entirely blank. + const nextLines = lines.map((s) => { + if (s.trim().length === 0) return ''; + + return ` ${s}`; + }); + + return `${firstLine}\n${nextLines.join('\n')}`; + }, + 'ul'(el, text) { + return text; + }, + 'ol'(el, text) { + // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. + return text.replace(/^- /mg, '1. '); + }, + 'h1'(el, text) { + return `# ${text.trim()}`; + }, + 'h2'(el, text) { + return `## ${text.trim()}`; + }, + 'h3'(el, text) { + return `### ${text.trim()}`; + }, + 'h4'(el, text) { + return `#### ${text.trim()}`; + }, + 'h5'(el, text) { + return `##### ${text.trim()}`; + }, + 'h6'(el, text) { + return `###### ${text.trim()}`; + }, + 'strong'(el, text) { + return `**${text}**`; + }, + 'em'(el, text) { + return `_${text}_`; + }, + 'del'(el, text) { + return `~~${text}~~`; + }, + 'sup'(el, text) { + return `^${text}`; + }, + 'hr'(el, text) { + return '-----'; + }, + 'table'(el, text) { + const theadEl = el.querySelector('thead'); + const tbodyEl = el.querySelector('tbody'); + if (!theadEl || !tbodyEl) return false; + + const theadText = CopyAsGFM.nodeToGFM(theadEl); + const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); + + return theadText + tbodyText; + }, + 'thead'(el, text) { + const cells = _.map(el.querySelectorAll('th'), (cell) => { + let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; + + let before = ''; + let after = ''; + switch (cell.style.textAlign) { + case 'center': + before = ':'; + after = ':'; + chars -= 2; + break; + case 'right': + after = ':'; + chars -= 1; + break; + default: + break; + } + + chars = Math.max(chars, 3); + + const middle = Array(chars + 1).join('-'); + + return before + middle + after; + }); + + return `${text}|${cells.join('|')}|`; + }, + 'tr'(el, text) { + const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); + return `| ${cells.join(' | ')} |`; + }, + }, +}; + +class CopyAsGFM { + constructor() { + $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); + } + + copyAsGFM(e, transformer) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const documentFragment = window.gl.utils.getSelectedFragment(); + if (!documentFragment) return; + + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return; + + e.preventDefault(); + e.stopPropagation(); + + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); + } + + pasteGFM(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const gfm = clipboardData.getData('text/x-gfm'); + if (!gfm) return; + + e.preventDefault(); + + window.gl.utils.insertText(e.target, gfm); + } + + static transformGFMSelection(documentFragment) { + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return null; + + return documentFragment; + } + + static transformCodeSelection(documentFragment) { + const lineEls = documentFragment.querySelectorAll('.line'); + + let codeEl; + if (lineEls.length > 1) { + codeEl = document.createElement('pre'); + codeEl.className = 'code highlight'; + + const lang = lineEls[0].getAttribute('lang'); + if (lang) { + codeEl.setAttribute('lang', lang); + } + } else { + codeEl = document.createElement('code'); + } + + if (lineEls.length > 0) { + for (let i = 0; i < lineEls.length; i += 1) { + const lineEl = lineEls[i]; + codeEl.appendChild(lineEl); + codeEl.appendChild(document.createTextNode('\n')); + } + } else { + codeEl.appendChild(documentFragment); + } + + return codeEl; + } + + static nodeToGFM(node) { + if (node.nodeType === Node.COMMENT_NODE) { + return ''; + } + + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent; + } + + const text = this.innerGFM(node); + + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return text; + } + + for (const filter in gfmRules) { + const rules = gfmRules[filter]; + + for (const selector in rules) { + const func = rules[selector]; + + if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + + const result = func(node, text); + if (result === false) continue; + + return result; + } + } + + return text; + } + + static innerGFM(parentNode) { + const nodes = parentNode.childNodes; + + const clonedParentNode = parentNode.cloneNode(true); + const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); + + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + const clonedNode = clonedNodes[i]; + + const text = this.nodeToGFM(node); + + // `clonedNode.replaceWith(text)` is not yet widely supported + clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); + } + + return clonedParentNode.innerText || clonedParentNode.textContent; + } +} + +window.gl = window.gl || {}; +window.gl.CopyAsGFM = CopyAsGFM; + +new CopyAsGFM(); diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 deleted file mode 100644 index 4bd537a6f28..00000000000 --- a/app/assets/javascripts/copy_as_gfm.js.es6 +++ /dev/null @@ -1,358 +0,0 @@ -/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ -/* jshint esversion: 6 */ - -require('./lib/utils/common_utils'); - -(() => { - const gfmRules = { - // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert - // GitLab Flavored Markdown (GFM) to HTML. - // These handlers consequently convert that same HTML to GFM to be copied to the clipboard. - // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML - // from GFM should have a handler here, in reverse order. - // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. - InlineDiffFilter: { - 'span.idiff.addition'(el, text) { - return `{+${text}+}`; - }, - 'span.idiff.deletion'(el, text) { - return `{-${text}-}`; - }, - }, - TaskListFilter: { - 'input[type=checkbox].task-list-item-checkbox'(el, text) { - return `[${el.checked ? 'x' : ' '}]`; - }, - }, - ReferenceFilter: { - 'a.gfm:not([data-link=true])'(el, text) { - return el.dataset.original || text; - }, - }, - AutolinkFilter: { - 'a'(el, text) { - // Fallback on the regular MarkdownFilter's `a` handler. - if (text !== el.getAttribute('href')) return false; - - return text; - }, - }, - TableOfContentsFilter: { - 'ul.section-nav'(el, text) { - return '[[_TOC_]]'; - }, - }, - EmojiFilter: { - 'img.emoji'(el, text) { - return el.getAttribute('alt'); - }, - }, - ImageLinkFilter: { - 'a.no-attachment-icon'(el, text) { - return text; - }, - }, - VideoLinkFilter: { - '.video-container'(el, text) { - const videoEl = el.querySelector('video'); - if (!videoEl) return false; - - return CopyAsGFM.nodeToGFM(videoEl); - }, - 'video'(el, text) { - return `![${el.dataset.title}](${el.getAttribute('src')})`; - }, - }, - MathFilter: { - 'pre.code.math[data-math-style=display]'(el, text) { - return `\`\`\`math\n${text.trim()}\n\`\`\``; - }, - 'code.code.math[data-math-style=inline]'(el, text) { - return `$\`${text}\`$`; - }, - 'span.katex-display span.katex-mathml'(el, text) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; - }, - 'span.katex-mathml'(el, text) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; - }, - 'span.katex-html'(el, text) { - // We don't want to include the content of this element in the copied text. - return ''; - }, - 'annotation[encoding="application/x-tex"]'(el, text) { - return text.trim(); - }, - }, - SanitizationFilter: { - 'a[name]:not([href]):empty'(el, text) { - return el.outerHTML; - }, - 'dl'(el, text) { - let lines = text.trim().split('\n'); - // Add two spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - lines = lines.map((l) => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }); - - return `<dl>\n${lines.join('\n')}\n</dl>`; - }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}</${tag}>`; - }, - }, - SyntaxHighlightFilter: { - 'pre.code.highlight'(el, t) { - const text = t.trim(); - - let lang = el.getAttribute('lang'); - if (lang === 'plaintext') { - lang = ''; - } - - // Prefixes lines with 4 spaces if the code contains triple backticks - if (lang === '' && text.match(/^```/gm)) { - return text.split('\n').map((l) => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }).join('\n'); - } - - return `\`\`\`${lang}\n${text}\n\`\`\``; - }, - 'pre > code'(el, text) { - // Don't wrap code blocks in `` - return text; - }, - }, - MarkdownFilter: { - 'br'(el, text) { - // Two spaces at the end of a line are turned into a BR - return ' '; - }, - 'code'(el, text) { - let backtickCount = 1; - const backtickMatch = text.match(/`+/); - if (backtickMatch) { - backtickCount = backtickMatch[0].length + 1; - } - - const backticks = Array(backtickCount + 1).join('`'); - const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - - return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; - }, - 'blockquote'(el, text) { - return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); - }, - 'img'(el, text) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; - }, - 'a.anchor'(el, text) { - // Don't render a Markdown link for the anchor link inside a heading - return text; - }, - 'a'(el, text) { - return `[${text}](${el.getAttribute('href')})`; - }, - 'li'(el, text) { - const lines = text.trim().split('\n'); - const firstLine = `- ${lines.shift()}`; - // Add four spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - const nextLines = lines.map((s) => { - if (s.trim().length === 0) return ''; - - return ` ${s}`; - }); - - return `${firstLine}\n${nextLines.join('\n')}`; - }, - 'ul'(el, text) { - return text; - }, - 'ol'(el, text) { - // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. - return text.replace(/^- /mg, '1. '); - }, - 'h1'(el, text) { - return `# ${text.trim()}`; - }, - 'h2'(el, text) { - return `## ${text.trim()}`; - }, - 'h3'(el, text) { - return `### ${text.trim()}`; - }, - 'h4'(el, text) { - return `#### ${text.trim()}`; - }, - 'h5'(el, text) { - return `##### ${text.trim()}`; - }, - 'h6'(el, text) { - return `###### ${text.trim()}`; - }, - 'strong'(el, text) { - return `**${text}**`; - }, - 'em'(el, text) { - return `_${text}_`; - }, - 'del'(el, text) { - return `~~${text}~~`; - }, - 'sup'(el, text) { - return `^${text}`; - }, - 'hr'(el, text) { - return '-----'; - }, - 'table'(el, text) { - const theadEl = el.querySelector('thead'); - const tbodyEl = el.querySelector('tbody'); - if (!theadEl || !tbodyEl) return false; - - const theadText = CopyAsGFM.nodeToGFM(theadEl); - const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - - return theadText + tbodyText; - }, - 'thead'(el, text) { - const cells = _.map(el.querySelectorAll('th'), (cell) => { - let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; - - let before = ''; - let after = ''; - switch (cell.style.textAlign) { - case 'center': - before = ':'; - after = ':'; - chars -= 2; - break; - case 'right': - after = ':'; - chars -= 1; - break; - default: - break; - } - - chars = Math.max(chars, 3); - - const middle = Array(chars + 1).join('-'); - - return before + middle + after; - }); - - return `${text}|${cells.join('|')}|`; - }, - 'tr'(el, text) { - const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); - return `| ${cells.join(' | ')} |`; - }, - }, - }; - - class CopyAsGFM { - constructor() { - $(document).on('copy', '.md, .wiki', this.handleCopy); - $(document).on('paste', '.js-gfm-input', this.handlePaste); - } - - handleCopy(e) { - const clipboardData = e.originalEvent.clipboardData; - if (!clipboardData) return; - - const documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) return; - - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; - - e.preventDefault(); - clipboardData.setData('text/plain', documentFragment.textContent); - - const gfm = CopyAsGFM.nodeToGFM(documentFragment); - clipboardData.setData('text/x-gfm', gfm); - } - - handlePaste(e) { - const clipboardData = e.originalEvent.clipboardData; - if (!clipboardData) return; - - const gfm = clipboardData.getData('text/x-gfm'); - if (!gfm) return; - - e.preventDefault(); - - window.gl.utils.insertText(e.target, gfm); - } - - static nodeToGFM(node) { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent; - } - - const text = this.innerGFM(node); - - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return text; - } - - for (const filter in gfmRules) { - const rules = gfmRules[filter]; - - for (const selector in rules) { - const func = rules[selector]; - - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; - - const result = func(node, text); - if (result === false) continue; - - return result; - } - } - - return text; - } - - static innerGFM(parentNode) { - const nodes = parentNode.childNodes; - - const clonedParentNode = parentNode.cloneNode(true); - const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); - - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - const clonedNode = clonedNodes[i]; - - const text = this.nodeToGFM(node); - - // `clonedNode.replaceWith(text)` is not yet widely supported - clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); - } - - return clonedParentNode.innerText || clonedParentNode.textContent; - } - } - - window.gl = window.gl || {}; - window.gl.CopyAsGFM = CopyAsGFM; - - new CopyAsGFM(); -})(); diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 615f485e18a..6dbec50b890 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,49 +1,46 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */ -/* global Clipboard */ - -window.Clipboard = require('vendor/clipboard'); - -(function() { - var genericError, genericSuccess, showTooltip; - - genericSuccess = function(e) { - showTooltip(e.trigger, 'Copied'); - // Clear the selection and blur the trigger so it loses its border - e.clearSelection(); - return $(e.trigger).blur(); - }; - - // Safari doesn't support `execCommand`, so instead we inform the user to - // copy manually. - // - // See http://clipboardjs.com/#browser-support - genericError = function(e) { - var key; - if (/Mac/i.test(navigator.userAgent)) { - key = '⌘'; // Command - } else { - key = 'Ctrl'; - } - return showTooltip(e.trigger, "Press " + key + "-C to copy"); - }; - - showTooltip = function(target, title) { - var $target = $(target); - var originalTitle = $target.data('original-title'); - - $target - .attr('title', 'Copied') - .tooltip('fixTitle') - .tooltip('show') - .attr('title', originalTitle) - .tooltip('fixTitle'); - }; - - $(function() { - var clipboard; - - clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); - clipboard.on('success', genericSuccess); - return clipboard.on('error', genericError); - }); -}).call(window); + +import Clipboard from 'vendor/clipboard'; + +var genericError, genericSuccess, showTooltip; + +genericSuccess = function(e) { + showTooltip(e.trigger, 'Copied'); + // Clear the selection and blur the trigger so it loses its border + e.clearSelection(); + return $(e.trigger).blur(); +}; + +// Safari doesn't support `execCommand`, so instead we inform the user to +// copy manually. +// +// See http://clipboardjs.com/#browser-support +genericError = function(e) { + var key; + if (/Mac/i.test(navigator.userAgent)) { + key = '⌘'; // Command + } else { + key = 'Ctrl'; + } + return showTooltip(e.trigger, "Press " + key + "-C to copy"); +}; + +showTooltip = function(target, title) { + var $target = $(target); + var originalTitle = $target.data('original-title'); + + $target + .attr('title', 'Copied') + .tooltip('fixTitle') + .tooltip('show') + .attr('title', originalTitle) + .tooltip('fixTitle'); +}; + +$(function() { + var clipboard; + + clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); + clipboard.on('success', genericSuccess); + return clipboard.on('error', genericError); +}); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js new file mode 100644 index 00000000000..121d64db789 --- /dev/null +++ b/app/assets/javascripts/create_label.js @@ -0,0 +1,127 @@ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* global Api */ + +class CreateLabelDropdown { + constructor ($el, namespacePath, projectPath) { + this.$el = $el; + this.namespacePath = namespacePath; + this.projectPath = projectPath; + this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); + this.$cancelButton = $('.js-cancel-label-btn', this.$el); + this.$newLabelField = $('#new_label_name', this.$el); + this.$newColorField = $('#new_label_color', this.$el); + this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el); + this.$newLabelError = $('.js-label-error', this.$el); + this.$newLabelCreateButton = $('.js-new-label-btn', this.$el); + this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el); + + this.$newLabelError.hide(); + this.$newLabelCreateButton.disable(); + + this.cleanBinding(); + this.addBinding(); + } + + cleanBinding () { + this.$colorSuggestions.off('click'); + this.$newLabelField.off('keyup change'); + this.$newColorField.off('keyup change'); + this.$dropdownBack.off('click'); + this.$cancelButton.off('click'); + this.$newLabelCreateButton.off('click'); + } + + addBinding () { + const self = this; + + this.$colorSuggestions.on('click', function (e) { + const $this = $(this); + self.addColorValue(e, $this); + }); + + this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this)); + this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this)); + + this.$dropdownBack.on('click', this.resetForm.bind(this)); + + this.$cancelButton.on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + self.resetForm(); + self.$dropdownBack.trigger('click'); + }); + + this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); + } + + addColorValue (e, $this) { + e.preventDefault(); + e.stopPropagation(); + + this.$newColorField.val($this.data('color')).trigger('change'); + this.$colorPreview + .css('background-color', $this.data('color')) + .parent() + .addClass('is-active'); + } + + enableLabelCreateButton () { + if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { + this.$newLabelError.hide(); + this.$newLabelCreateButton.enable(); + } else { + this.$newLabelCreateButton.disable(); + } + } + + resetForm () { + this.$newLabelField + .val('') + .trigger('change'); + + this.$newColorField + .val('') + .trigger('change'); + + this.$colorPreview + .css('background-color', '') + .parent() + .removeClass('is-active'); + } + + saveLabel (e) { + e.preventDefault(); + e.stopPropagation(); + + Api.newLabel(this.namespacePath, this.projectPath, { + title: this.$newLabelField.val(), + color: this.$newColorField.val() + }, (label) => { + this.$newLabelCreateButton.enable(); + + if (label.message) { + let errors; + + if (typeof label.message === 'string') { + errors = label.message; + } else { + errors = Object.keys(label.message).map(key => + `${gl.text.humanize(key)} ${label.message[key].join(', ')}` + ).join("<br/>"); + } + + this.$newLabelError + .html(errors) + .show(); + } else { + this.$dropdownBack.trigger('click'); + + $(document).trigger('created.label', label); + } + }); + } +} + +window.gl = window.gl || {}; +gl.CreateLabelDropdown = CreateLabelDropdown; diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 deleted file mode 100644 index 85384d98126..00000000000 --- a/app/assets/javascripts/create_label.js.es6 +++ /dev/null @@ -1,132 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ -/* global Api */ - -(function (w) { - class CreateLabelDropdown { - constructor ($el, namespacePath, projectPath) { - this.$el = $el; - this.namespacePath = namespacePath; - this.projectPath = projectPath; - this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown')); - this.$cancelButton = $('.js-cancel-label-btn', this.$el); - this.$newLabelField = $('#new_label_name', this.$el); - this.$newColorField = $('#new_label_color', this.$el); - this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el); - this.$newLabelError = $('.js-label-error', this.$el); - this.$newLabelCreateButton = $('.js-new-label-btn', this.$el); - this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el); - - this.$newLabelError.hide(); - this.$newLabelCreateButton.disable(); - - this.cleanBinding(); - this.addBinding(); - } - - cleanBinding () { - this.$colorSuggestions.off('click'); - this.$newLabelField.off('keyup change'); - this.$newColorField.off('keyup change'); - this.$dropdownBack.off('click'); - this.$cancelButton.off('click'); - this.$newLabelCreateButton.off('click'); - } - - addBinding () { - const self = this; - - this.$colorSuggestions.on('click', function (e) { - const $this = $(this); - self.addColorValue(e, $this); - }); - - this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this)); - this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this)); - - this.$dropdownBack.on('click', this.resetForm.bind(this)); - - this.$cancelButton.on('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - - self.resetForm(); - self.$dropdownBack.trigger('click'); - }); - - this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); - } - - addColorValue (e, $this) { - e.preventDefault(); - e.stopPropagation(); - - this.$newColorField.val($this.data('color')).trigger('change'); - this.$colorPreview - .css('background-color', $this.data('color')) - .parent() - .addClass('is-active'); - } - - enableLabelCreateButton () { - if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { - this.$newLabelError.hide(); - this.$newLabelCreateButton.enable(); - } else { - this.$newLabelCreateButton.disable(); - } - } - - resetForm () { - this.$newLabelField - .val('') - .trigger('change'); - - this.$newColorField - .val('') - .trigger('change'); - - this.$colorPreview - .css('background-color', '') - .parent() - .removeClass('is-active'); - } - - saveLabel (e) { - e.preventDefault(); - e.stopPropagation(); - - Api.newLabel(this.namespacePath, this.projectPath, { - title: this.$newLabelField.val(), - color: this.$newColorField.val() - }, (label) => { - this.$newLabelCreateButton.enable(); - - if (label.message) { - let errors; - - if (typeof label.message === 'string') { - errors = label.message; - } else { - errors = Object.keys(label.message).map(key => - `${gl.text.humanize(key)} ${label.message[key].join(', ')}` - ).join("<br/>"); - } - - this.$newLabelError - .html(errors) - .show(); - } else { - this.$dropdownBack.trigger('click'); - - $(document).trigger('created.label', label); - } - }); - } - } - - if (!w.gl) { - w.gl = {}; - } - - gl.CreateLabelDropdown = CreateLabelDropdown; -})(window); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index b83a4c63fad..b83a4c63fad 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index cb1687dcc7a..cb1687dcc7a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js new file mode 100644 index 00000000000..42e1bbce744 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -0,0 +1,56 @@ +/* eslint-disable no-param-reassign */ +import Vue from 'vue'; +import iconCommit from '../svg/icon_commit.svg'; + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StagePlanComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + + data() { + return { iconCommit }; + }, + + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + <span v-if="items.length === 50" class="events-info pull-right"> + <i class="fa fa-warning has-tooltip" + title="Limited to showing 50 events at most" + data-placement="top"></i> + Showing 50 events + </span> + </div> + <ul class="stage-event-list"> + <li v-for="commit in items" class="stage-event-item"> + <div class="item-details item-conmmit-component"> + <img class="avatar" :src="commit.author.avatarUrl"> + <h5 class="item-title commit-title"> + <a :href="commit.commitUrl"> + {{ commit.title }} + </a> + </h5> + <span> + First + <span class="commit-icon">${iconCommit}</span> + <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> + pushed by + <a :href="commit.author.webUrl" class="commit-author-link"> + {{ commit.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="commit.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 deleted file mode 100644 index 8652479e7bf..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StagePlanComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - <span v-if="items.length === 50" class="events-info pull-right"> - <i class="fa fa-warning has-tooltip" - title="Limited to showing 50 events at most" - data-placement="top"></i> - Showing 50 events - </span> - </div> - <ul class="stage-event-list"> - <li v-for="commit in items" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <img class="avatar" :src="commit.author.avatarUrl"> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> - {{ commit.title }} - </a> - </h5> - <span> - First - <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span> - <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> - pushed by - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="commit.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index 73f4205b578..73f4205b578 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 501ffb1fac9..501ffb1fac9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js new file mode 100644 index 00000000000..8fa63734cf1 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -0,0 +1,48 @@ +/* eslint-disable no-param-reassign */ +import Vue from 'vue'; +import iconBranch from '../svg/icon_branch.svg'; + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageStagingComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + data() { + return { iconBranch }; + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <img class="avatar" :src="build.author.avatarUrl"> + <h5 class="item-title"> + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="build-date">{{ build.date }}</a> + by + <a :href="build.author.webUrl" class="issue-author-link"> + {{ build.author.name }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 deleted file mode 100644 index 82622232f64..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageStagingComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <img class="avatar" :src="build.author.avatarUrl"> - <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> - <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> - by - <a :href="build.author.webUrl" class="issue-author-link"> - {{ build.author.name }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js new file mode 100644 index 00000000000..0015249cfaa --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js @@ -0,0 +1,49 @@ +/* eslint-disable no-param-reassign */ +import Vue from 'vue'; +import iconBuildStatus from '../svg/icon_build_status.svg'; +import iconBranch from '../svg/icon_branch.svg'; + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + global.cycleAnalytics.StageTestComponent = Vue.extend({ + props: { + items: Array, + stage: Object, + }, + data() { + return { iconBuildStatus, iconBranch }; + }, + template: ` + <div> + <div class="events-description"> + {{ stage.description }} + </div> + <ul class="stage-event-list"> + <li v-for="build in items" class="stage-event-item item-build-component"> + <div class="item-details"> + <h5 class="item-title"> + <span class="icon-build-status">${iconBuildStatus}</span> + <a :href="build.url" class="item-build-name">{{ build.name }}</a> + · + <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <i class="fa fa-code-fork"></i> + <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> + <span class="icon-branch">${iconBranch}</span> + <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> + </h5> + <span> + <a :href="build.url" class="issue-date"> + {{ build.date }} + </a> + </span> + </div> + <div class="item-time"> + <total-time :time="build.totalTime"></total-time> + </div> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 deleted file mode 100644 index 4bfd363a1f1..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - global.cycleAnalytics.StageTestComponent = Vue.extend({ - props: { - items: Array, - stage: Object, - }, - template: ` - <div> - <div class="events-description"> - {{ stage.description }} - </div> - <ul class="stage-event-list"> - <li v-for="build in items" class="stage-event-item item-build-component"> - <div class="item-details"> - <h5 class="item-title"> - <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> - · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> - <i class="fa fa-code-fork"></i> - <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a> - <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span> - <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a> - </h5> - <span> - <a :href="build.url" class="issue-date"> - {{ build.date }} - </a> - </span> - </div> - <div class="item-time"> - <total-time :time="build.totalTime"></total-time> - </div> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/total_time_component.js index 0d85e1a4678..0d85e1a4678 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6 +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js new file mode 100644 index 00000000000..beff293b587 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -0,0 +1,135 @@ +/* global Vue */ +/* global Cookies */ +/* global Flash */ + +window.Vue = require('vue'); +window.Cookies = require('js-cookie'); +require('./components/stage_code_component'); +require('./components/stage_issue_component'); +require('./components/stage_plan_component'); +require('./components/stage_production_component'); +require('./components/stage_review_component'); +require('./components/stage_staging_component'); +require('./components/stage_test_component'); +require('./components/total_time_component'); +require('./cycle_analytics_service'); +require('./cycle_analytics_store'); +require('./default_event_objects'); + +$(() => { + const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); + const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; + const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ + requestPath: cycleAnalyticsEl.dataset.requestPath, + }); + + gl.cycleAnalyticsApp = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + data: { + state: cycleAnalyticsStore.state, + isLoading: false, + isLoadingStage: false, + isEmptyStage: false, + hasError: false, + startDate: 30, + isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), + }, + computed: { + currentStage() { + return cycleAnalyticsStore.currentActiveStage(); + }, + }, + components: { + 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, + 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, + 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, + 'stage-test-component': gl.cycleAnalytics.StageTestComponent, + 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, + 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, + 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, + }, + created() { + this.fetchCycleAnalyticsData(); + }, + methods: { + handleError() { + cycleAnalyticsStore.setErrorState(true); + return new Flash('There was an error while fetching cycle analytics data.'); + }, + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').off('click').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + this.startDate = $target.data('value'); + + $label.text($target.text().trim()); + this.fetchCycleAnalyticsData({ startDate: this.startDate }); + }); + }, + fetchCycleAnalyticsData(options) { + const fetchOptions = options || { startDate: this.startDate }; + + this.isLoading = true; + + cycleAnalyticsService + .fetchCycleAnalyticsData(fetchOptions) + .done((response) => { + cycleAnalyticsStore.setCycleAnalyticsData(response); + this.selectDefaultStage(); + this.initDropdown(); + }) + .error(() => { + this.handleError(); + }) + .always(() => { + this.isLoading = false; + }); + }, + selectDefaultStage() { + const stage = this.state.stages.first(); + this.selectStage(stage); + }, + selectStage(stage) { + if (this.isLoadingStage) return; + if (this.currentStage === stage) return; + + if (!stage.isUserAllowed) { + cycleAnalyticsStore.setActiveStage(stage); + return; + } + + this.isLoadingStage = true; + cycleAnalyticsStore.setStageEvents([], stage); + cycleAnalyticsStore.setActiveStage(stage); + + cycleAnalyticsService + .fetchStageData({ + stage, + startDate: this.startDate, + }) + .done((response) => { + this.isEmptyStage = !response.events.length; + cycleAnalyticsStore.setStageEvents(response.events, stage); + }) + .error(() => { + this.isEmptyStage = true; + }) + .always(() => { + this.isLoadingStage = false; + }); + }, + dismissOverviewDialog() { + this.isOverviewDialogDismissed = true; + Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); + }, + }, + }); + + // Register global components + Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); +}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 deleted file mode 100644 index 1ac715aab77..00000000000 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ /dev/null @@ -1,128 +0,0 @@ -/* global Vue */ -/* global Cookies */ -/* global Flash */ - -window.Vue = require('vue'); -window.Cookies = require('js-cookie'); - -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); - -$(() => { - const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); - const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore; - const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({ - requestPath: cycleAnalyticsEl.dataset.requestPath, - }); - - gl.cycleAnalyticsApp = new Vue({ - el: '#cycle-analytics', - name: 'CycleAnalytics', - data: { - state: cycleAnalyticsStore.state, - isLoading: false, - isLoadingStage: false, - isEmptyStage: false, - hasError: false, - startDate: 30, - isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE), - }, - computed: { - currentStage() { - return cycleAnalyticsStore.currentActiveStage(); - }, - }, - components: { - 'stage-issue-component': gl.cycleAnalytics.StageIssueComponent, - 'stage-plan-component': gl.cycleAnalytics.StagePlanComponent, - 'stage-code-component': gl.cycleAnalytics.StageCodeComponent, - 'stage-test-component': gl.cycleAnalytics.StageTestComponent, - 'stage-review-component': gl.cycleAnalytics.StageReviewComponent, - 'stage-staging-component': gl.cycleAnalytics.StageStagingComponent, - 'stage-production-component': gl.cycleAnalytics.StageProductionComponent, - }, - created() { - this.fetchCycleAnalyticsData(); - }, - methods: { - handleError() { - cycleAnalyticsStore.setErrorState(true); - return new Flash('There was an error while fetching cycle analytics data.'); - }, - initDropdown() { - const $dropdown = $('.js-ca-dropdown'); - const $label = $dropdown.find('.dropdown-label'); - - $dropdown.find('li a').off('click').on('click', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - this.startDate = $target.data('value'); - - $label.text($target.text().trim()); - this.fetchCycleAnalyticsData({ startDate: this.startDate }); - }); - }, - fetchCycleAnalyticsData(options) { - const fetchOptions = options || { startDate: this.startDate }; - - this.isLoading = true; - - cycleAnalyticsService - .fetchCycleAnalyticsData(fetchOptions) - .done((response) => { - cycleAnalyticsStore.setCycleAnalyticsData(response); - this.selectDefaultStage(); - this.initDropdown(); - }) - .error(() => { - this.handleError(); - }) - .always(() => { - this.isLoading = false; - }); - }, - selectDefaultStage() { - const stage = this.state.stages.first(); - this.selectStage(stage); - }, - selectStage(stage) { - if (this.isLoadingStage) return; - if (this.currentStage === stage) return; - - if (!stage.isUserAllowed) { - cycleAnalyticsStore.setActiveStage(stage); - return; - } - - this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([], stage); - cycleAnalyticsStore.setActiveStage(stage); - - cycleAnalyticsService - .fetchStageData({ - stage, - startDate: this.startDate, - }) - .done((response) => { - this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events, stage); - }) - .error(() => { - this.isEmptyStage = true; - }) - .always(() => { - this.isLoadingStage = false; - }); - }, - dismissOverviewDialog() { - this.isOverviewDialogDismissed = true; - Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); - }, - }, - }); - - // Register global components - Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); -}); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index 9f74b14c4b9..9f74b14c4b9 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js new file mode 100644 index 00000000000..7ae9de7297c --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -0,0 +1,104 @@ +/* eslint-disable no-param-reassign */ + +require('../lib/utils/text_utility'); +const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); + +((global) => { + global.cycleAnalytics = global.cycleAnalytics || {}; + + const EMPTY_STAGE_TEXTS = { + issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + }; + + global.cycleAnalytics.CycleAnalyticsStore = { + state: { + summary: '', + stats: '', + analytics: '', + events: [], + stages: [], + }, + setCycleAnalyticsData(data) { + this.state = Object.assign(this.state, this.decorateData(data)); + }, + decorateData(data) { + const newData = {}; + + newData.stages = data.stats || []; + newData.summary = data.summary || []; + + newData.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + newData.stages.forEach((item) => { + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); + item.active = false; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; + }); + newData.analytics = data; + return newData; + }, + setLoadingState(state) { + this.state.isLoading = state; + }, + setErrorState(state) { + this.state.hasError = state; + }, + deactivateAllStages() { + this.state.stages.forEach((stage) => { + stage.active = false; + }); + }, + setActiveStage(stage) { + this.deactivateAllStages(); + stage.active = true; + }, + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); + }, + decorateEvents(events, stage) { + const newEvents = []; + + events.forEach((item) => { + if (!item) return; + + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + + eventItem.totalTime = eventItem.total_time; + + if (eventItem.author) { + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; + } + + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; + + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; + + newEvents.push(eventItem); + }); + + return newEvents; + }, + currentActiveStage() { + return this.state.stages.find(stage => stage.active); + }, + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 deleted file mode 100644 index 3efeb141008..00000000000 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable no-param-reassign */ - -require('../lib/utils/text_utility'); -const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); - -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - - const EMPTY_STAGE_TEXTS = { - issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', - plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', - code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', - test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', - review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', - staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', - production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', - }; - - global.cycleAnalytics.CycleAnalyticsStore = { - state: { - summary: '', - stats: '', - analytics: '', - events: [], - stages: [], - }, - setCycleAnalyticsData(data) { - this.state = Object.assign(this.state, this.decorateData(data)); - }, - decorateData(data) { - const newData = {}; - - newData.stages = data.stats || []; - newData.summary = data.summary || []; - - newData.summary.forEach((item) => { - item.value = item.value || '-'; - }); - - newData.stages.forEach((item) => { - const stageSlug = gl.text.dasherize(item.title.toLowerCase()); - item.active = false; - item.isUserAllowed = data.permissions[stageSlug]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; - item.component = `stage-${stageSlug}-component`; - item.slug = stageSlug; - }); - newData.analytics = data; - return newData; - }, - setLoadingState(state) { - this.state.isLoading = state; - }, - setErrorState(state) { - this.state.hasError = state; - }, - deactivateAllStages() { - this.state.stages.forEach((stage) => { - stage.active = false; - }); - }, - setActiveStage(stage) { - this.deactivateAllStages(); - stage.active = true; - }, - setStageEvents(events, stage) { - this.state.events = this.decorateEvents(events, stage); - }, - decorateEvents(events, stage) { - const newEvents = []; - - events.forEach((item) => { - if (!item) return; - - const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); - - eventItem.totalTime = eventItem.total_time; - eventItem.author.webUrl = eventItem.author.web_url; - eventItem.author.avatarUrl = eventItem.author.avatar_url; - - if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; - if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; - if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - - delete eventItem.author.web_url; - delete eventItem.author.avatar_url; - delete eventItem.total_time; - delete eventItem.created_at; - delete eventItem.short_sha; - delete eventItem.commit_url; - - newEvents.push(eventItem); - }); - - return newEvents; - }, - currentActiveStage() { - return this.state.stages.find(stage => stage.active); - }, - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js index cfaf9835bf8..cfaf9835bf8 100644 --- a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 deleted file mode 100644 index 5d486bcaf66..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg new file mode 100644 index 00000000000..9f547d3d744 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg> diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 deleted file mode 100644 index 661bf9e9f1c..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg new file mode 100644 index 00000000000..b932d90618a --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg> diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 deleted file mode 100644 index 2208c27a619..00000000000 --- a/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6 +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - global.cycleAnalytics = global.cycleAnalytics || {}; - global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {}; - - global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>'; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg new file mode 100644 index 00000000000..6a517756058 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg> diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js new file mode 100644 index 00000000000..cfa60325fcc --- /dev/null +++ b/app/assets/javascripts/diff.js @@ -0,0 +1,128 @@ +/* eslint-disable class-methods-use-this */ + +require('./lib/utils/url_utility'); + +const UNFOLD_COUNT = 20; +let isBound = false; + +class Diff { + constructor() { + const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); + $diffFile.filesCommentButton(); + + $diffFile.each((index, file) => new gl.ImageFile(file)); + + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } + + if (!isBound) { + $(document) + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + isBound = true; + } + + if (gl.utils.getLocationHash()) { + this.highlightSelectedLine(); + } + + this.openAnchoredDiff(); + } + + handleClickUnfold(e) { + const $target = $(e.target); + // current babel config relies on iterators implementation, so we cannot simply do: + // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + const ref = this.lineNumbers($target.parent()); + const oldLineNumber = ref[0]; + const newLineNumber = ref[1]; + const offset = newLineNumber - oldLineNumber; + const bottom = $target.hasClass('js-unfold-bottom'); + let since; + let to; + let unfold = true; + + if (bottom) { + const lineNumber = newLineNumber + 1; + since = lineNumber; + to = lineNumber + UNFOLD_COUNT; + } else { + const lineNumber = newLineNumber - 1; + since = lineNumber - UNFOLD_COUNT; + to = lineNumber; + + // make sure we aren't loading more than we need + const prevNewLine = this.lineNumbers($target.parent().prev())[1]; + if (since <= prevNewLine + 1) { + since = prevNewLine + 1; + unfold = false; + } + } + + const file = $target.parents('.diff-file'); + const link = file.data('blob-diff-path'); + const view = file.data('view'); + + const params = { since, to, bottom, offset, unfold, view }; + $.get(link, params, response => $target.parent().replaceWith(response)); + } + + openAnchoredDiff(cb) { + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + + if (!anchoredDiff) return; + + const diffTitle = $(`#${anchoredDiff}`); + const diffFile = diffTitle.closest('.diff-file'); + const nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + const clickTarget = $('.js-file-title, .click-to-expand', diffFile); + diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { + this.highlightSelectedLine(); + if (cb) cb(); + }); + } else if (cb) { + cb(); + } + } + + handleClickLineNum(e) { + const hash = $(e.currentTarget).attr('href'); + e.preventDefault(); + if (window.history.pushState) { + window.history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlightSelectedLine(); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + lineNumbers(line) { + if (!line.children().length) { + return [0, 0]; + } + return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); + } + + highlightSelectedLine() { + const hash = gl.utils.getLocationHash(); + const $diffFiles = $('.diff-file'); + $diffFiles.find('.hll').removeClass('hll'); + + if (hash) { + $diffFiles + .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) + .addClass('hll'); + } + } +} + +window.gl = window.gl || {}; +window.gl.Diff = Diff; diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 deleted file mode 100644 index ccccd0a36ff..00000000000 --- a/app/assets/javascripts/diff.js.es6 +++ /dev/null @@ -1,126 +0,0 @@ -/* eslint-disable class-methods-use-this */ - -require('./lib/utils/url_utility'); - -(() => { - const UNFOLD_COUNT = 20; - let isBound = false; - - class Diff { - constructor() { - const $diffFile = $('.files .diff-file'); - $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); - - $diffFile.each((index, file) => new gl.ImageFile(file)); - - if (this.diffViewType() === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } - - if (!isBound) { - $(document) - .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) - .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); - isBound = true; - } - - this.openAnchoredDiff(); - } - - handleClickUnfold(e) { - const $target = $(e.target); - // current babel config relies on iterators implementation, so we cannot simply do: - // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); - const ref = this.lineNumbers($target.parent()); - const oldLineNumber = ref[0]; - const newLineNumber = ref[1]; - const offset = newLineNumber - oldLineNumber; - const bottom = $target.hasClass('js-unfold-bottom'); - let since; - let to; - let unfold = true; - - if (bottom) { - const lineNumber = newLineNumber + 1; - since = lineNumber; - to = lineNumber + UNFOLD_COUNT; - } else { - const lineNumber = newLineNumber - 1; - since = lineNumber - UNFOLD_COUNT; - to = lineNumber; - - // make sure we aren't loading more than we need - const prevNewLine = this.lineNumbers($target.parent().prev())[1]; - if (since <= prevNewLine + 1) { - since = prevNewLine + 1; - unfold = false; - } - } - - const file = $target.parents('.diff-file'); - const link = file.data('blob-diff-path'); - const view = file.data('view'); - - const params = { since, to, bottom, offset, unfold, view }; - $.get(link, params, response => $target.parent().replaceWith(response)); - } - - openAnchoredDiff(cb) { - const locationHash = gl.utils.getLocationHash(); - const anchoredDiff = locationHash && locationHash.split('_')[0]; - - if (!anchoredDiff) return; - - const diffTitle = $(`#${anchoredDiff}`); - const diffFile = diffTitle.closest('.diff-file'); - const nothingHereBlock = $('.nothing-here-block:visible', diffFile); - if (nothingHereBlock.length) { - const clickTarget = $('.js-file-title, .click-to-expand', diffFile); - diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { - this.highlighSelectedLine(); - if (cb) cb(); - }); - } else if (cb) { - cb(); - } - } - - handleClickLineNum(e) { - const hash = $(e.currentTarget).attr('href'); - e.preventDefault(); - if (window.history.pushState) { - window.history.pushState(null, null, hash); - } else { - window.location.hash = hash; - } - this.highlighSelectedLine(); - } - - diffViewType() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } - - lineNumbers(line) { - if (!line.children().length) { - return [0, 0]; - } - return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); - } - - highlighSelectedLine() { - const hash = gl.utils.getLocationHash(); - const $diffFiles = $('.diff-file'); - $diffFiles.find('.hll').removeClass('hll'); - - if (hash) { - $diffFiles - .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) - .addClass('hll'); - } - } - } - - window.gl = window.gl || {}; - window.gl.Diff = Diff; -})(); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index d948dff58ec..d948dff58ec 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js new file mode 100644 index 00000000000..dd7081aefb7 --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -0,0 +1,156 @@ +/* global CommentsStore Cookies notes */ +import Vue from 'vue'; +import collapseIcon from '../icons/collapse_icon.svg'; + +(() => { + const DiffNoteAvatars = Vue.extend({ + props: ['discussionId'], + data() { + return { + isVisible: false, + lineType: '', + storeState: CommentsStore.state, + shownAvatars: 3, + collapseIcon, + }; + }, + template: ` + <div class="diff-comment-avatar-holders" + v-show="notesCount !== 0"> + <div v-if="!isVisible"> + <img v-for="note in notesSubset" + class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" + width="19" + height="19" + role="button" + data-container="body" + data-placement="top" + data-html="true" + :data-line-type="lineType" + :title="note.authorName + ': ' + note.noteTruncated" + :src="note.authorAvatar" + @click="clickedAvatar($event)" /> + <span v-if="notesCount > shownAvatars" + class="diff-comments-more-count has-tooltip js-diff-comment-avatar" + data-container="body" + data-placement="top" + ref="extraComments" + role="button" + :data-line-type="lineType" + :title="extraNotesTitle" + @click="clickedAvatar($event)">{{ moreText }}</span> + </div> + <button class="diff-notes-collapse js-diff-comment-avatar" + type="button" + aria-label="Show comments" + :data-line-type="lineType" + @click="clickedAvatar($event)" + v-if="isVisible" + v-html="collapseIcon"> + </button> + </div> + `, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { + this.$nextTick(() => { + this.setDiscussionVisible(); + }); + }); + }, + destroyed() { + $(document).off('toggle.comments'); + }, + watch: { + storeState: { + handler() { + this.$nextTick(() => { + $('.has-tooltip', this.$el).tooltip('fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); + }); + }, + deep: true, + }, + }, + computed: { + notesSubset() { + let notes = []; + + if (this.discussion) { + notes = Object.keys(this.discussion.notes) + .slice(0, this.shownAvatars) + .map(noteId => this.discussion.notes[noteId]); + } + + return notes; + }, + extraNotesTitle() { + if (this.discussion) { + const extra = this.discussion.notesCount() - this.shownAvatars; + + return `${extra} more comment${extra > 1 ? 's' : ''}`; + } + + return ''; + }, + discussion() { + return this.storeState[this.discussionId]; + }, + notesCount() { + if (this.discussion) { + return this.discussion.notesCount(); + } + + return 0; + }, + moreText() { + const plusSign = this.notesCount < 100 ? '+' : ''; + + return `${plusSign}${this.notesCount - this.shownAvatars}`; + }, + }, + methods: { + clickedAvatar(e) { + notes.addDiffNote(e); + + // Toggle the active state of the toggle all button + this.toggleDiscussionsToggleState(); + + this.$nextTick(() => { + this.setDiscussionVisible(); + + $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('hide'); + }); + }, + addNoCommentClass() { + const notesCount = this.notesCount; + + $(this.$el).closest('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0) + .nextUntil('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0); + }, + toggleDiscussionsToggleState() { + const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); + const $visibleNotesHolders = $notesHolders.filter(':visible'); + const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); + + $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); + }, + setDiscussionVisible() { + this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); + }, + }, + }); + + Vue.component('diff-note-avatars', DiffNoteAvatars); +})(); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 283dc330cad..283dc330cad 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js new file mode 100644 index 00000000000..e86bef47172 --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js @@ -0,0 +1,29 @@ +/* global Vue */ +/* global CommentsStore */ + +(() => { + const NewIssueForDiscussion = Vue.extend({ + props: { + discussionId: { + type: String, + required: true, + }, + }, + data() { + return { + discussions: CommentsStore.state, + }; + }, + computed: { + discussion() { + return this.discussions[this.discussionId]; + }, + showButton() { + if (this.discussion) return !this.discussion.isResolved(); + return false; + }, + }, + }); + + Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js new file mode 100644 index 00000000000..fbd980f0fce --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -0,0 +1,120 @@ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* global CommentsStore */ +/* global ResolveService */ +/* global Flash */ +const Vue = require('vue'); + +(() => { + const ResolveBtn = Vue.extend({ + props: { + noteId: Number, + discussionId: String, + resolved: Boolean, + canResolve: Boolean, + resolvedBy: String, + authorName: String, + authorAvatar: String, + noteTruncated: String, + }, + data: function () { + return { + discussions: CommentsStore.state, + loading: false, + note: {}, + }; + }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, + computed: { + discussion: function () { + return this.discussions[this.discussionId]; + }, + buttonText: function () { + if (this.isResolved) { + return `Resolved by ${this.resolvedByName}`; + } else if (this.canResolve) { + return 'Mark as resolved'; + } else { + return 'Unable to resolve'; + } + }, + isResolved: function () { + if (this.note) { + return this.note.resolved; + } else { + return false; + } + }, + resolvedByName: function () { + return this.note.resolved_by; + }, + }, + methods: { + updateTooltip: function () { + this.$nextTick(() => { + $(this.$refs.button) + .tooltip('hide') + .tooltip('fixTitle'); + }); + }, + resolve: function () { + if (!this.canResolve) return; + + let promise; + this.loading = true; + + if (this.isResolved) { + promise = ResolveService + .unresolve(this.noteId); + } else { + promise = ResolveService + .resolve(this.noteId); + } + + promise.then((response) => { + this.loading = false; + + if (response.status === 200) { + const data = response.json(); + const resolved_by = data ? data.resolved_by : null; + + CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); + this.discussion.updateHeadline(data); + } else { + new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); + } + + this.updateTooltip(); + }); + } + }, + mounted: function () { + $(this.$refs.button).tooltip({ + container: 'body' + }); + }, + beforeDestroy: function () { + CommentsStore.delete(this.discussionId, this.noteId); + }, + created: function () { + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); + + this.note = this.discussion.getNote(this.noteId); + } + }); + + Vue.component('resolve-btn', ResolveBtn); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 deleted file mode 100644 index d1873d6c7a2..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ -/* global CommentsStore */ -/* global ResolveService */ -/* global Flash */ -const Vue = require('vue'); - -(() => { - const ResolveBtn = Vue.extend({ - props: { - noteId: Number, - discussionId: String, - resolved: Boolean, - canResolve: Boolean, - resolvedBy: String - }, - data: function () { - return { - discussions: CommentsStore.state, - loading: false, - note: {}, - }; - }, - watch: { - 'discussions': { - handler: 'updateTooltip', - deep: true - } - }, - computed: { - discussion: function () { - return this.discussions[this.discussionId]; - }, - buttonText: function () { - if (this.isResolved) { - return `Resolved by ${this.resolvedByName}`; - } else if (this.canResolve) { - return 'Mark as resolved'; - } else { - return 'Unable to resolve'; - } - }, - isResolved: function () { - if (this.note) { - return this.note.resolved; - } else { - return false; - } - }, - resolvedByName: function () { - return this.note.resolved_by; - }, - }, - methods: { - updateTooltip: function () { - this.$nextTick(() => { - $(this.$refs.button) - .tooltip('hide') - .tooltip('fixTitle'); - }); - }, - resolve: function () { - if (!this.canResolve) return; - - let promise; - this.loading = true; - - if (this.isResolved) { - promise = ResolveService - .unresolve(this.noteId); - } else { - promise = ResolveService - .resolve(this.noteId); - } - - promise.then((response) => { - this.loading = false; - - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; - - CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); - this.discussion.updateHeadline(data); - } else { - new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); - } - - this.updateTooltip(); - }); - } - }, - mounted: function () { - $(this.$refs.button).tooltip({ - container: 'body' - }); - }, - beforeDestroy: function () { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created: function () { - CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); - - this.note = this.discussion.getNote(this.noteId); - } - }); - - Vue.component('resolve-btn', ResolveBtn); -})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js index de9367f2136..de9367f2136 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js index 7c5fcd04d2d..7c5fcd04d2d 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js new file mode 100644 index 00000000000..4f6b86a917c --- /dev/null +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -0,0 +1,68 @@ +/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */ +/* global Vue */ +/* global ResolveCount */ + +const Vue = require('vue'); +require('./models/discussion'); +require('./models/note'); +require('./stores/comments'); +require('./services/resolve'); +require('./mixins/discussion'); +require('./components/comment_resolve_btn'); +require('./components/jump_to_discussion'); +require('./components/resolve_btn'); +require('./components/resolve_count'); +require('./components/resolve_discussion_btn'); +require('./components/diff_note_avatars'); +require('./components/new_issue_for_discussion'); + +$(() => { + const projectPath = document.querySelector('.merge-request').dataset.projectPath; + const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; + + window.gl = window.gl || {}; + window.gl.diffNoteApps = {}; + + window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); + + gl.diffNotesCompileComponents = () => { + $('diff-note-avatars').each(function () { + const tmp = Vue.extend({ + template: $(this).get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + $(this).replaceWith(tmpApp.$el); + }); + + const $components = $(COMPONENT_SELECTOR).filter(function () { + return $(this).closest('resolve-count').length !== 1; + }); + + if ($components) { + $components.each(function () { + const $this = $(this); + const noteId = $this.attr(':note-id'); + const tmp = Vue.extend({ + template: $this.get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + if (noteId) { + gl.diffNoteApps[`note_${noteId}`] = tmpApp; + } + + $this.replaceWith(tmpApp.$el); + }); + } + }; + + gl.diffNotesCompileComponents(); + + new Vue({ + el: '#resolve-count-app', + components: { + 'resolve-count': ResolveCount + } + }); +}); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 deleted file mode 100644 index 190461451d5..00000000000 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ -/* global Vue */ -/* global ResolveCount */ - -function requireAll(context) { return context.keys().map(context); } -const Vue = require('vue'); -requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); - -$(() => { - const projectPath = document.querySelector('.merge-request').dataset.projectPath; - const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; - - window.gl = window.gl || {}; - window.gl.diffNoteApps = {}; - - window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); - - gl.diffNotesCompileComponents = () => { - const $components = $(COMPONENT_SELECTOR).filter(function () { - return $(this).closest('resolve-count').length !== 1; - }); - - if ($components) { - $components.each(function () { - const $this = $(this); - const noteId = $this.attr(':note-id'); - const tmp = Vue.extend({ - template: $this.get(0).outerHTML - }); - const tmpApp = new tmp().$mount(); - - if (noteId) { - gl.diffNoteApps[`note_${noteId}`] = tmpApp; - } - - $this.replaceWith(tmpApp.$el); - }); - } - }; - - gl.diffNotesCompileComponents(); - - new Vue({ - el: '#resolve-count-app', - components: { - 'resolve-count': ResolveCount - } - }); -}); diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg new file mode 100644 index 00000000000..bd4b393cfaa --- /dev/null +++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg @@ -0,0 +1 @@ +<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg> diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js index 3c08c222f46..3c08c222f46 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js new file mode 100644 index 00000000000..dce1a9b58bd --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -0,0 +1,96 @@ +/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ +/* global Vue */ +/* global NoteModel */ + +class DiscussionModel { + constructor (discussionId) { + this.id = discussionId; + this.notes = {}; + this.loading = false; + this.canResolve = false; + } + + createNote (noteObj) { + Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj)); + } + + deleteNote (noteId) { + Vue.delete(this.notes, noteId); + } + + getNote (noteId) { + return this.notes[noteId]; + } + + notesCount() { + return Object.keys(this.notes).length; + } + + isResolved () { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + return false; + } + } + return true; + } + + resolveAllNotes (resolved_by) { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + note.resolved = true; + note.resolved_by = resolved_by; + } + } + } + + unResolveAllNotes () { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (note.resolved) { + note.resolved = false; + note.resolved_by = null; + } + } + } + + updateHeadline (data) { + const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; + const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); + + if (data.discussion_headline_html) { + if ($discussionHeadline.length) { + $discussionHeadline.replaceWith(data.discussion_headline_html); + } else { + $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); + } + + gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); + } else { + $discussionHeadline.remove(); + } + } + + isResolvable () { + if (!this.canResolve) { + return false; + } + + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (note.canResolve) { + return true; + } + } + + return false; + } +} + +window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6 deleted file mode 100644 index fa518ba4d33..00000000000 --- a/app/assets/javascripts/diff_notes/models/discussion.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ -/* global Vue */ -/* global NoteModel */ - -class DiscussionModel { - constructor (discussionId) { - this.id = discussionId; - this.notes = {}; - this.loading = false; - this.canResolve = false; - } - - createNote (noteId, canResolve, resolved, resolved_by) { - Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); - } - - deleteNote (noteId) { - Vue.delete(this.notes, noteId); - } - - getNote (noteId) { - return this.notes[noteId]; - } - - notesCount() { - return Object.keys(this.notes).length; - } - - isResolved () { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - return false; - } - } - return true; - } - - resolveAllNotes (resolved_by) { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - note.resolved = true; - note.resolved_by = resolved_by; - } - } - } - - unResolveAllNotes () { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.resolved) { - note.resolved = false; - note.resolved_by = null; - } - } - } - - updateHeadline (data) { - const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; - const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); - - if (data.discussion_headline_html) { - if ($discussionHeadline.length) { - $discussionHeadline.replaceWith(data.discussion_headline_html); - } else { - $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); - } - - gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); - } else { - $discussionHeadline.remove(); - } - } - - isResolvable () { - if (!this.canResolve) { - return false; - } - - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.canResolve) { - return true; - } - } - - return false; - } -} - -window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js new file mode 100644 index 00000000000..04465aa507e --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/note.js @@ -0,0 +1,16 @@ +/* eslint-disable camelcase, no-unused-vars */ + +class NoteModel { + constructor(discussionId, noteObj) { + this.discussionId = discussionId; + this.id = noteObj.noteId; + this.canResolve = noteObj.canResolve; + this.resolved = noteObj.resolved; + this.resolved_by = noteObj.resolvedBy; + this.authorName = noteObj.authorName; + this.authorAvatar = noteObj.authorAvatar; + this.noteTruncated = noteObj.noteTruncated; + } +} + +window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6 deleted file mode 100644 index f3a7cba5ef6..00000000000 --- a/app/assets/javascripts/diff_notes/models/note.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable camelcase, no-unused-vars */ - -class NoteModel { - constructor(discussionId, noteId, canResolve, resolved, resolved_by) { - this.discussionId = discussionId; - this.id = noteId; - this.canResolve = canResolve; - this.resolved = resolved; - this.resolved_by = resolved_by; - } -} - -window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js index 090c454e9e4..090c454e9e4 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js new file mode 100644 index 00000000000..69c4d7a8434 --- /dev/null +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -0,0 +1,57 @@ +/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* global Vue */ +/* global DiscussionModel */ + +((w) => { + w.CommentsStore = { + state: {}, + get: function (discussionId, noteId) { + return this.state[discussionId].getNote(noteId); + }, + createDiscussion: function (discussionId, canResolve) { + let discussion = this.state[discussionId]; + if (!this.state[discussionId]) { + discussion = new DiscussionModel(discussionId); + Vue.set(this.state, discussionId, discussion); + } + + if (canResolve !== undefined) { + discussion.canResolve = canResolve; + } + + return discussion; + }, + create: function (noteObj) { + const discussion = this.createDiscussion(noteObj.discussionId); + + discussion.createNote(noteObj); + }, + update: function (discussionId, noteId, resolved, resolved_by) { + const discussion = this.state[discussionId]; + const note = discussion.getNote(noteId); + note.resolved = resolved; + note.resolved_by = resolved_by; + }, + delete: function (discussionId, noteId) { + const discussion = this.state[discussionId]; + discussion.deleteNote(noteId); + + if (discussion.notesCount() === 0) { + Vue.delete(this.state, discussionId); + } + }, + unresolvedDiscussionIds: function () { + const ids = []; + + for (const discussionId in this.state) { + const discussion = this.state[discussionId]; + + if (!discussion.isResolved()) { + ids.push(discussion.id); + } + } + + return ids; + } + }; +})(window); diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 deleted file mode 100644 index c80d979b977..00000000000 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ -/* global Vue */ -/* global DiscussionModel */ - -((w) => { - w.CommentsStore = { - state: {}, - get: function (discussionId, noteId) { - return this.state[discussionId].getNote(noteId); - }, - createDiscussion: function (discussionId, canResolve) { - let discussion = this.state[discussionId]; - if (!this.state[discussionId]) { - discussion = new DiscussionModel(discussionId); - Vue.set(this.state, discussionId, discussion); - } - - if (canResolve !== undefined) { - discussion.canResolve = canResolve; - } - - return discussion; - }, - create: function (discussionId, noteId, canResolve, resolved, resolved_by) { - const discussion = this.createDiscussion(discussionId); - - discussion.createNote(noteId, canResolve, resolved, resolved_by); - }, - update: function (discussionId, noteId, resolved, resolved_by) { - const discussion = this.state[discussionId]; - const note = discussion.getNote(noteId); - note.resolved = resolved; - note.resolved_by = resolved_by; - }, - delete: function (discussionId, noteId) { - const discussion = this.state[discussionId]; - discussion.deleteNote(noteId); - - if (discussion.notesCount() === 0) { - Vue.delete(this.state, discussionId); - } - }, - unresolvedDiscussionIds: function () { - const ids = []; - - for (const discussionId in this.state) { - const discussion = this.state[discussionId]; - - if (!discussion.isResolved()) { - ids.push(discussion.id); - } - } - - return ids; - } - }; -})(window); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js new file mode 100644 index 00000000000..3557f6f617e --- /dev/null +++ b/app/assets/javascripts/dispatcher.js @@ -0,0 +1,448 @@ +/* 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 */ +/* global UsernameValidator */ +/* global ActiveTabMemoizer */ +/* global ShortcutsNavigation */ +/* global Build */ +/* global Issuable */ +/* global ShortcutsIssuable */ +/* global ZenMode */ +/* global Milestone */ +/* global IssuableForm */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global MergedButtons */ +/* global Commit */ +/* global NotificationsForm */ +/* global TreeView */ +/* global NotificationsDropdown */ +/* global UsersSelect */ +/* global GroupAvatar */ +/* global LineHighlighter */ +/* global ProjectFork */ +/* global BuildArtifacts */ +/* global GroupsSelect */ +/* global Search */ +/* global Admin */ +/* global NamespaceSelects */ +/* global ShortcutsDashboardNavigation */ +/* global Project */ +/* global ProjectAvatar */ +/* global CompareAutocomplete */ +/* global ProjectNew */ +/* global Star */ +/* global ProjectShow */ +/* global Labels */ +/* global Shortcuts */ +import Issue from './issue'; + +import BindInOut from './behaviors/bind_in_out'; +import GroupName from './group_name'; +import GroupsList from './groups_list'; +import ProjectsList from './projects_list'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; +import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; + +const ShortcutsBlob = require('./shortcuts_blob'); +const UserCallout = require('./user_callout'); + +(function() { + var Dispatcher; + + $(function() { + return new Dispatcher(); + }); + + Dispatcher = (function() { + function Dispatcher() { + this.initSearch(); + this.initFieldErrors(); + this.initPageScripts(); + } + + Dispatcher.prototype.initPageScripts = function() { + var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; + page = $('body').attr('data-page'); + if (!page) { + return false; + } + path = page.split(':'); + shortcut_handler = null; + + function initBlob() { + new LineHighlighter(); + + new BlobLinePermalinkUpdater( + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + shortcut_handler = new ShortcutsNavigation(); + fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + new ShortcutsBlob({ + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + } + + switch (page) { + case 'sessions:new': + new UsernameValidator(); + new ActiveTabMemoizer(); + break; + case 'projects:boards:show': + case 'projects:boards:index': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:builds:show': + new Build(); + break; + case 'projects:merge_requests:index': + case 'projects:issues:index': + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); + } + Issuable.init(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', + }); + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:issues:show': + new Issue(); + shortcut_handler = new ShortcutsIssuable(); + new ZenMode(); + break; + case 'projects:milestones:show': + case 'groups:milestones:show': + case 'dashboard:milestones:show': + new Milestone(); + break; + case 'dashboard:todos:index': + new gl.Todos(); + break; + case 'dashboard:projects:index': + case 'dashboard:projects:starred': + case 'explore:projects:index': + case 'explore:projects:trending': + case 'explore:projects:starred': + case 'admin:projects:index': + new ProjectsList(); + break; + case 'dashboard:groups:index': + case 'explore:groups:index': + new GroupsList(); + break; + case 'projects:milestones:new': + case 'projects:milestones:edit': + case 'projects:milestones:update': + new ZenMode(); + new gl.DueDateSelectors(); + new gl.GLForm($('.milestone-form')); + break; + case 'groups:milestones:new': + new ZenMode(); + break; + case 'projects:compare:show': + new gl.Diff(); + break; + case 'projects:branches:index': + gl.AjaxLoadingSpinner.init(); + break; + case 'projects:issues:new': + case 'projects:issues:edit': + shortcut_handler = new ShortcutsNavigation(); + new gl.GLForm($('.issue-form')); + new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); + break; + case 'projects:merge_requests:new': + case 'projects:merge_requests:new_diffs': + case 'projects:merge_requests:edit': + new gl.Diff(); + shortcut_handler = new ShortcutsNavigation(); + new gl.GLForm($('.merge-request-form')); + new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); + new gl.IssuableTemplateSelectors(); + break; + case 'projects:tags:new': + new ZenMode(); + new gl.GLForm($('.tag-form')); + break; + case 'projects:releases:edit': + new ZenMode(); + new gl.GLForm($('.release-form')); + break; + case 'projects:merge_requests:show': + new gl.Diff(); + shortcut_handler = new ShortcutsIssuable(true); + new ZenMode(); + new MergedButtons(); + break; + case 'projects:merge_requests:commits': + new MergedButtons(); + break; + case "projects:merge_requests:diffs": + new gl.Diff(); + new ZenMode(); + new MergedButtons(); + break; + case 'dashboard:activity': + new gl.Activities(); + break; + case 'projects:commit:show': + new Commit(); + new gl.Diff(); + new ZenMode(); + shortcut_handler = new ShortcutsNavigation(); + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + break; + case 'projects:commit:pipelines': + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + break; + case 'projects:commits:show': + case 'projects:activity': + shortcut_handler = new ShortcutsNavigation(); + break; + case 'projects:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + if ($('#tree-slider').length) { + new TreeView(); + } + break; + case 'projects:pipelines:builds': + case 'projects:pipelines:show': + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + + new gl.Pipelines({ + initTabs: true, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); + break; + case 'groups:activity': + new gl.Activities(); + break; + case 'groups:show': + shortcut_handler = new ShortcutsNavigation(); + new NotificationsForm(); + new NotificationsDropdown(); + new ProjectsList(); + break; + case 'groups:group_members:index': + new gl.MemberExpirationDate(); + new gl.Members(); + new UsersSelect(); + break; + case 'projects:members:show': + new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + new GroupsSelect(); + new gl.MemberExpirationDate(); + new gl.Members(); + new UsersSelect(); + break; + case 'groups:new': + case 'admin:groups:new': + case 'groups:create': + case 'admin:groups:create': + BindInOut.initAll(); + case 'groups:new': + case 'admin:groups:new': + case 'groups:edit': + case 'admin:groups:edit': + new GroupAvatar(); + break; + case 'projects:tree:show': + shortcut_handler = new ShortcutsNavigation(); + new TreeView(); + gl.TargetBranchDropDown.bootstrap(); + break; + case 'projects:find_file:show': + shortcut_handler = true; + break; + case 'projects:blob:new': + gl.TargetBranchDropDown.bootstrap(); + break; + case 'projects:blob:create': + gl.TargetBranchDropDown.bootstrap(); + break; + case 'projects:blob:show': + gl.TargetBranchDropDown.bootstrap(); + initBlob(); + break; + case 'projects:blob:edit': + gl.TargetBranchDropDown.bootstrap(); + break; + case 'projects:blame:show': + initBlob(); + break; + case 'groups:labels:new': + case 'groups:labels:edit': + case 'projects:labels:new': + case 'projects:labels:edit': + new Labels(); + break; + case 'projects:labels:index': + if ($('.prioritized-labels').length) { + new gl.LabelManager(); + } + break; + case 'projects:network:show': + // Ensure we don't create a particular shortcut handler here. This is + // already created, where the network graph is created. + shortcut_handler = true; + break; + case 'projects:forks:new': + new ProjectFork(); + break; + case 'projects:artifacts:browse': + new BuildArtifacts(); + break; + case 'help:index': + gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + break; + case 'search:show': + new Search(); + break; + case 'projects:repository:show': + new gl.ProtectedBranchCreate(); + new gl.ProtectedBranchEditList(); + break; + case 'projects:ci_cd:show': + new gl.ProjectVariables(); + break; + case 'ci:lints:create': + case 'ci:lints:show': + new gl.CILintEditor(); + break; + case 'users:show': + new UserCallout(); + break; + } + switch (path.first()) { + case 'sessions': + case 'omniauth_callbacks': + if (!gon.u2f) break; + gl.u2fAuthenticate = new gl.U2FAuthenticate( + $('#js-authenticate-u2f'), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + gl.u2fAuthenticate.start(); + case 'admin': + new Admin(); + switch (path[1]) { + case 'groups': + new UsersSelect(); + break; + case 'projects': + new NamespaceSelects(); + break; + case 'labels': + switch (path[2]) { + case 'new': + case 'edit': + new Labels(); + } + case 'abuse_reports': + new gl.AbuseReports(); + break; + } + break; + case 'dashboard': + case 'root': + shortcut_handler = new ShortcutsDashboardNavigation(); + new UserCallout(); + break; + case 'groups': + new GroupName(); + break; + case 'profiles': + new NotificationsForm(); + new NotificationsDropdown(); + break; + case 'projects': + new Project(); + new ProjectAvatar(); + new GroupName(); + switch (path[1]) { + case 'compare': + new CompareAutocomplete(); + break; + case 'edit': + shortcut_handler = new ShortcutsNavigation(); + new ProjectNew(); + break; + case 'new': + new ProjectNew(); + break; + case 'show': + new Star(); + new ProjectNew(); + new ProjectShow(); + new NotificationsDropdown(); + break; + case 'wikis': + new gl.Wikis(); + shortcut_handler = new ShortcutsNavigation(); + new ZenMode(); + new gl.GLForm($('.wiki-form')); + break; + case 'snippets': + shortcut_handler = new ShortcutsNavigation(); + if (path[2] === 'show') { + new ZenMode(); + } + break; + case 'labels': + case 'graphs': + case 'compare': + case 'pipelines': + case 'forks': + case 'milestones': + case 'project_members': + case 'deploy_keys': + case 'builds': + case 'hooks': + case 'services': + case 'protected_branches': + shortcut_handler = new ShortcutsNavigation(); + } + } + // If we haven't installed a custom shortcut handler, install the default one + if (!shortcut_handler) { + new Shortcuts(); + } + }; + + Dispatcher.prototype.initSearch = function() { + // Only when search form is present + if ($('.search').length) { + return new gl.SearchAutocomplete(); + } + }; + + Dispatcher.prototype.initFieldErrors = function() { + $('.gl-show-field-errors').each((i, form) => { + new gl.GlFieldErrors(form); + }); + }; + + return Dispatcher; + })(); +}).call(window); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 deleted file mode 100644 index f55db02f0fd..00000000000 --- a/app/assets/javascripts/dispatcher.js.es6 +++ /dev/null @@ -1,386 +0,0 @@ -/* 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 */ -/* global UsernameValidator */ -/* global ActiveTabMemoizer */ -/* global ShortcutsNavigation */ -/* global Build */ -/* global Issuable */ -/* global Issue */ -/* global ShortcutsIssuable */ -/* global ZenMode */ -/* global Milestone */ -/* global IssuableForm */ -/* global LabelsSelect */ -/* global MilestoneSelect */ -/* global MergedButtons */ -/* global Commit */ -/* global NotificationsForm */ -/* global TreeView */ -/* global NotificationsDropdown */ -/* global UsersSelect */ -/* global GroupAvatar */ -/* global LineHighlighter */ -/* global ProjectFork */ -/* global BuildArtifacts */ -/* global GroupsSelect */ -/* global Search */ -/* global Admin */ -/* global NamespaceSelects */ -/* global ShortcutsDashboardNavigation */ -/* global Project */ -/* global ProjectAvatar */ -/* global CompareAutocomplete */ -/* global ProjectNew */ -/* global Star */ -/* global ProjectShow */ -/* global Labels */ -/* global Shortcuts */ - -const ShortcutsBlob = require('./shortcuts_blob'); - -(function() { - var Dispatcher; - - $(function() { - return new Dispatcher(); - }); - - Dispatcher = (function() { - function Dispatcher() { - this.initSearch(); - this.initFieldErrors(); - this.initPageScripts(); - } - - Dispatcher.prototype.initPageScripts = function() { - var page, path, shortcut_handler; - page = $('body').attr('data-page'); - if (!page) { - return false; - } - path = page.split(':'); - shortcut_handler = null; - switch (page) { - case 'sessions:new': - new UsernameValidator(); - new ActiveTabMemoizer(); - break; - case 'projects:boards:show': - case 'projects:boards:index': - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:builds:show': - new Build(); - break; - case 'projects:merge_requests:index': - case 'projects:issues:index': - if (gl.FilteredSearchManager) { - new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); - } - Issuable.init(); - new gl.IssuableBulkActions({ - prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', - }); - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:issues:show': - new Issue(); - shortcut_handler = new ShortcutsIssuable(); - new ZenMode(); - break; - case 'projects:milestones:show': - case 'groups:milestones:show': - case 'dashboard:milestones:show': - new Milestone(); - break; - case 'dashboard:todos:index': - new gl.Todos(); - break; - case 'projects:milestones:new': - case 'projects:milestones:edit': - case 'projects:milestones:update': - new ZenMode(); - new gl.DueDateSelectors(); - new gl.GLForm($('.milestone-form')); - break; - case 'groups:milestones:new': - new ZenMode(); - break; - case 'projects:compare:show': - new gl.Diff(); - break; - case 'projects:issues:new': - case 'projects:issues:edit': - shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.issue-form')); - new IssuableForm($('.issue-form')); - new LabelsSelect(); - new MilestoneSelect(); - new gl.IssuableTemplateSelectors(); - break; - case 'projects:merge_requests:new': - case 'projects:merge_requests:new_diffs': - case 'projects:merge_requests:edit': - new gl.Diff(); - shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.merge-request-form')); - new IssuableForm($('.merge-request-form')); - new LabelsSelect(); - new MilestoneSelect(); - new gl.IssuableTemplateSelectors(); - break; - case 'projects:tags:new': - new ZenMode(); - new gl.GLForm($('.tag-form')); - break; - case 'projects:releases:edit': - new ZenMode(); - new gl.GLForm($('.release-form')); - break; - case 'projects:merge_requests:show': - new gl.Diff(); - shortcut_handler = new ShortcutsIssuable(true); - new ZenMode(); - new MergedButtons(); - break; - case 'projects:merge_requests:commits': - new MergedButtons(); - break; - case "projects:merge_requests:diffs": - new gl.Diff(); - new ZenMode(); - new MergedButtons(); - break; - case 'dashboard:activity': - new gl.Activities(); - break; - case 'dashboard:projects:starred': - new gl.Activities(); - break; - case 'projects:commit:show': - new Commit(); - new gl.Diff(); - new ZenMode(); - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }).bindEvents(); - break; - case 'projects:commits:show': - case 'projects:activity': - shortcut_handler = new ShortcutsNavigation(); - break; - case 'projects:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - if ($('#tree-slider').length) { - new TreeView(); - } - break; - case 'projects:pipelines:builds': - case 'projects:pipelines:show': - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - - new gl.Pipelines({ - initTabs: true, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); - break; - case 'groups:activity': - new gl.Activities(); - break; - case 'groups:show': - shortcut_handler = new ShortcutsNavigation(); - new NotificationsForm(); - new NotificationsDropdown(); - break; - case 'groups:group_members:index': - new gl.MemberExpirationDate(); - new gl.Members(); - new UsersSelect(); - break; - case 'projects:members:show': - new gl.MemberExpirationDate('.js-access-expiration-date-groups'); - new GroupsSelect(); - new gl.MemberExpirationDate(); - new gl.Members(); - new UsersSelect(); - break; - case 'groups:new': - case 'groups:edit': - case 'admin:groups:edit': - case 'admin:groups:new': - new GroupAvatar(); - break; - case 'projects:tree:show': - shortcut_handler = new ShortcutsNavigation(); - new TreeView(); - break; - case 'projects:find_file:show': - shortcut_handler = true; - break; - case 'projects:blob:show': - case 'projects:blame:show': - new LineHighlighter(); - shortcut_handler = new ShortcutsNavigation(); - const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); - break; - case 'groups:labels:new': - case 'groups:labels:edit': - case 'projects:labels:new': - case 'projects:labels:edit': - new Labels(); - break; - case 'projects:labels:index': - if ($('.prioritized-labels').length) { - new gl.LabelManager(); - } - break; - case 'projects:network:show': - // Ensure we don't create a particular shortcut handler here. This is - // already created, where the network graph is created. - shortcut_handler = true; - break; - case 'projects:forks:new': - new ProjectFork(); - break; - case 'projects:artifacts:browse': - new BuildArtifacts(); - break; - case 'help:index': - gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); - break; - case 'search:show': - new Search(); - break; - case 'projects:protected_branches:index': - new gl.ProtectedBranchCreate(); - new gl.ProtectedBranchEditList(); - break; - case 'projects:ci_cd:show': - new gl.ProjectVariables(); - break; - case 'ci:lints:create': - case 'ci:lints:show': - new gl.CILintEditor(); - break; - } - switch (path.first()) { - case 'sessions': - case 'omniauth_callbacks': - if (!gon.u2f) break; - gl.u2fAuthenticate = new gl.U2FAuthenticate( - $('#js-authenticate-u2f'), - '#js-login-u2f-form', - gon.u2f, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - gl.u2fAuthenticate.start(); - case 'admin': - new Admin(); - switch (path[1]) { - case 'groups': - new UsersSelect(); - break; - case 'projects': - new NamespaceSelects(); - break; - case 'labels': - switch (path[2]) { - case 'new': - case 'edit': - new Labels(); - } - case 'abuse_reports': - new gl.AbuseReports(); - break; - } - break; - case 'dashboard': - case 'root': - shortcut_handler = new ShortcutsDashboardNavigation(); - break; - case 'profiles': - new NotificationsForm(); - new NotificationsDropdown(); - break; - case 'projects': - new Project(); - new ProjectAvatar(); - switch (path[1]) { - case 'compare': - new CompareAutocomplete(); - break; - case 'edit': - shortcut_handler = new ShortcutsNavigation(); - new ProjectNew(); - break; - case 'new': - new ProjectNew(); - break; - case 'show': - new Star(); - new ProjectNew(); - new ProjectShow(); - new NotificationsDropdown(); - break; - case 'wikis': - new gl.Wikis(); - shortcut_handler = new ShortcutsNavigation(); - new ZenMode(); - new gl.GLForm($('.wiki-form')); - break; - case 'snippets': - shortcut_handler = new ShortcutsNavigation(); - if (path[2] === 'show') { - new ZenMode(); - } - break; - case 'labels': - case 'graphs': - case 'compare': - case 'pipelines': - case 'forks': - case 'milestones': - case 'project_members': - case 'deploy_keys': - case 'builds': - case 'hooks': - case 'services': - case 'protected_branches': - shortcut_handler = new ShortcutsNavigation(); - } - } - // If we haven't installed a custom shortcut handler, install the default one - if (!shortcut_handler) { - new Shortcuts(); - } - }; - - Dispatcher.prototype.initSearch = function() { - // Only when search form is present - if ($('.search').length) { - return new gl.SearchAutocomplete(); - } - }; - - Dispatcher.prototype.initFieldErrors = function() { - $('.gl-show-field-errors').each((i, form) => { - new gl.GlFieldErrors(form); - }); - }; - - return Dispatcher; - })(); -}).call(window); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 5cdf11c6a2c..020f8b4ac65 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -37,11 +37,14 @@ require('../window')(function(w){ } } - self.hook.list[config.method].call(self.hook.list, data); + if (!self.destroyed) { + self.hook.list[config.method].call(self.hook.list, data); + } }, init: function init(hook) { var self = this; + self.destroyed = false; self.cache = self.cache || {}; var config = hook.config.droplabAjax; this.hook = hook; @@ -71,6 +74,9 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint) .then(function(d) { self._loadData(d, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }).catch(function(e) { throw new droplabAjaxException(e.message || e); }); @@ -79,6 +85,7 @@ require('../window')(function(w){ destroy: function() { var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + this.destroyed = true; if (this.listTemplate && dynamicList) { dynamicList.outerHTML = this.listTemplate; } diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index b63d73066cb..05eba7aef56 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -82,6 +82,9 @@ require('../window')(function(w){ this._loadUrlData(url) .then(function(data) { self._loadData(data, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }); } }, diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 64a7a9eaf37..f2963a5eb19 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -3,217 +3,216 @@ require('./preview_markdown'); -(function() { - this.DropzoneInput = (function() { - function DropzoneInput(form) { - var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; - Dropzone.autoDiscover = false; - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; - divHover = "<div class=\"div-dropzone-hover\"></div>"; - divSpinner = "<div class=\"div-dropzone-spinner\"></div>"; - divAlert = "<div class=\"" + alertClass + "\"></div>"; - iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"; - iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; - uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); - btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>"; - project_uploads_path = window.project_uploads_path || null; - max_file_size = gon.max_file_size || 10; - form_textarea = $(form).find(".js-gfm-input"); - form_textarea.wrap("<div class=\"div-dropzone\"></div>"); - form_textarea.on('paste', (function(_this) { - return function(event) { - return handlePaste(event); - }; - })(this)); - $mdArea = $(form_textarea).closest('.md-area'); - $(form).setupMarkdownPreview(); - form_dropzone = $(form).find('.div-dropzone'); - form_dropzone.parent().addClass("div-dropzone-wrapper"); - form_dropzone.append(divHover); - form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); - form_dropzone.append(divSpinner); - form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); - form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - dropzone = form_dropzone.dropzone({ - url: project_uploads_path, - dictDefaultMessage: "", - clickable: true, - paramName: "file", - maxFilesize: max_file_size, - uploadMultiple: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - previewContainer: false, - processing: function() { - return $(".div-dropzone-alert").alert("close"); - }, - dragover: function() { - $mdArea.addClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0.7); - }, - dragleave: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - }, - drop: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - form_textarea.focus(); - }, - success: function(header, response) { - pasteText(response.link.markdown); - }, - error: function(temp) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); - } - }, - totaluploadprogress: function(totalUploadProgress) { - uploadProgress.text(Math.round(totalUploadProgress) + "%"); - }, - sending: function() { - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); - }, - queuecomplete: function() { - uploadProgress.text(""); - $(".dz-preview").remove(); - $(".markdown-area").trigger("input"); - $(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - } - }); - child = $(dropzone[0]).children("textarea"); - handlePaste = function(event) { - var filename, image, pasteEvent, text; - pasteEvent = event.originalEvent; - if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { - image = isImage(pasteEvent); - if (image) { - event.preventDefault(); - filename = getFilename(pasteEvent) || "image.png"; - text = "{{" + filename + "}}"; - pasteText(text); - return uploadFile(image.getAsFile(), filename); - } - } +window.DropzoneInput = (function() { + function DropzoneInput(form) { + var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; + Dropzone.autoDiscover = false; + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; + divHover = "<div class=\"div-dropzone-hover\"></div>"; + divSpinner = "<div class=\"div-dropzone-spinner\"></div>"; + divAlert = "<div class=\"" + alertClass + "\"></div>"; + iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"; + iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; + uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); + btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>"; + project_uploads_path = window.project_uploads_path || null; + max_file_size = gon.max_file_size || 10; + form_textarea = $(form).find(".js-gfm-input"); + form_textarea.wrap("<div class=\"div-dropzone\"></div>"); + form_textarea.on('paste', (function(_this) { + return function(event) { + return handlePaste(event); }; - isImage = function(data) { - var i, item; - i = 0; - while (i < data.clipboardData.items.length) { - item = data.clipboardData.items[i]; - if (item.type.indexOf("image") !== -1) { - return item; - } - i += 1; - } - return false; - }; - pasteText = function(text) { - var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; - caretStart = $(child)[0].selectionStart; - caretEnd = $(child)[0].selectionEnd; - textEnd = $(child).val().length; - beforeSelection = $(child).val().substring(0, caretStart); - afterSelection = $(child).val().substring(caretEnd, textEnd); - $(child).val(beforeSelection + text + afterSelection); - child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length); - return form_textarea.trigger("input"); - }; - getFilename = function(e) { - var value; - if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData("Text"); - } else if (e.clipboardData && e.clipboardData.getData) { - value = e.clipboardData.getData("text/plain"); + })(this)); + $mdArea = $(form_textarea).closest('.md-area'); + $(form).setupMarkdownPreview(); + form_dropzone = $(form).find('.div-dropzone'); + form_dropzone.parent().addClass("div-dropzone-wrapper"); + form_dropzone.append(divHover); + form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); + form_dropzone.append(divSpinner); + form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); + form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + dropzone = form_dropzone.dropzone({ + url: project_uploads_path, + dictDefaultMessage: "", + clickable: true, + paramName: "file", + maxFilesize: max_file_size, + uploadMultiple: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + previewContainer: false, + processing: function() { + return $(".div-dropzone-alert").alert("close"); + }, + dragover: function() { + $mdArea.addClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0.7); + }, + dragleave: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + }, + drop: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + form_textarea.focus(); + }, + success: function(header, response) { + pasteText(response.link.markdown); + }, + error: function(temp) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); } - value = value.split("\r"); - return value.first(); - }; - uploadFile = function(item, filename) { - var formData; - formData = new FormData(); - formData.append("file", item, filename); - return $.ajax({ - url: project_uploads_path, - type: "POST", - data: formData, - dataType: "json", - processData: false, - contentType: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - beforeSend: function() { - showSpinner(); - return closeAlertMessage(); - }, - success: function(e, textStatus, response) { - return insertToTextArea(filename, response.responseJSON.link.markdown); - }, - error: function(response) { - return showError(response.responseJSON.message); - }, - complete: function() { - return closeSpinner(); - } - }); - }; - insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url + "\n"); - }); - }; - appendToTextArea = function(url) { - return $(child).val(function(index, val) { - return val + url + "\n"; - }); - }; - showSpinner = function(e) { - return form.find(".div-dropzone-spinner").css({ + }, + totaluploadprogress: function(totalUploadProgress) { + uploadProgress.text(Math.round(totalUploadProgress) + "%"); + }, + sending: function() { + form_dropzone.find(".div-dropzone-spinner").css({ "opacity": 0.7, "display": "inherit" }); - }; - closeSpinner = function() { - return form.find(".div-dropzone-spinner").css({ + }, + queuecomplete: function() { + uploadProgress.text(""); + $(".dz-preview").remove(); + $(".markdown-area").trigger("input"); + $(".div-dropzone-spinner").css({ "opacity": 0, "display": "none" }); - }; - showError = function(message) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - return $(".div-dropzone-alert").append(btnAlert + message); + } + }); + child = $(dropzone[0]).children("textarea"); + handlePaste = function(event) { + var filename, image, pasteEvent, text; + pasteEvent = event.originalEvent; + if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { + image = isImage(pasteEvent); + if (image) { + event.preventDefault(); + filename = getFilename(pasteEvent) || "image.png"; + text = "{{" + filename + "}}"; + pasteText(text); + return uploadFile(image.getAsFile(), filename); } - }; - closeAlertMessage = function() { - return form.find(".div-dropzone-alert").alert("close"); - }; - form.find(".markdown-selector").click(function(e) { - e.preventDefault(); - $(this).closest('.gfm-form').find('.div-dropzone').click(); + } + }; + isImage = function(data) { + var i, item; + i = 0; + while (i < data.clipboardData.items.length) { + item = data.clipboardData.items[i]; + if (item.type.indexOf("image") !== -1) { + return item; + } + i += 1; + } + return false; + }; + pasteText = function(text) { + var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + var formattedText = text + "\n\n"; + caretStart = $(child)[0].selectionStart; + caretEnd = $(child)[0].selectionEnd; + textEnd = $(child).val().length; + beforeSelection = $(child).val().substring(0, caretStart); + afterSelection = $(child).val().substring(caretEnd, textEnd); + $(child).val(beforeSelection + formattedText + afterSelection); + child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); + return form_textarea.trigger("input"); + }; + getFilename = function(e) { + var value; + if (window.clipboardData && window.clipboardData.getData) { + value = window.clipboardData.getData("Text"); + } else if (e.clipboardData && e.clipboardData.getData) { + value = e.clipboardData.getData("text/plain"); + } + value = value.split("\r"); + return value.first(); + }; + uploadFile = function(item, filename) { + var formData; + formData = new FormData(); + formData.append("file", item, filename); + return $.ajax({ + url: project_uploads_path, + type: "POST", + data: formData, + dataType: "json", + processData: false, + contentType: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + beforeSend: function() { + showSpinner(); + return closeAlertMessage(); + }, + success: function(e, textStatus, response) { + return insertToTextArea(filename, response.responseJSON.link.markdown); + }, + error: function(response) { + return showError(response.responseJSON.message); + }, + complete: function() { + return closeSpinner(); + } + }); + }; + insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace("{{" + filename + "}}", url + "\n"); + }); + }; + appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + showSpinner = function(e) { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }; + closeSpinner = function() { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" }); - } + }; + showError = function(message) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + return $(".div-dropzone-alert").append(btnAlert + message); + } + }; + closeAlertMessage = function() { + return form.find(".div-dropzone-alert").alert("close"); + }; + form.find(".markdown-selector").click(function(e) { + e.preventDefault(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); + }); + } - return DropzoneInput; - })(); -}).call(window); + return DropzoneInput; +})(); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js new file mode 100644 index 00000000000..db10b383913 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js @@ -0,0 +1,203 @@ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ +/* global dateFormat */ +/* global Pikaday */ + +class DueDateSelect { + constructor({ $dropdown, $loading } = {}) { + const $dropdownParent = $dropdown.closest('.dropdown'); + const $block = $dropdown.closest('.block'); + this.$loading = $loading; + this.$dropdown = $dropdown; + this.$dropdownParent = $dropdownParent; + this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); + this.$block = $block; + this.$selectbox = $dropdown.closest('.selectbox'); + this.$value = $block.find('.value'); + this.$valueContent = $block.find('.value-content'); + this.$sidebarValue = $('.js-due-date-sidebar-value', $block); + this.fieldName = $dropdown.data('field-name'), + this.abilityName = $dropdown.data('ability-name'), + this.issueUpdateURL = $dropdown.data('issue-update'); + + this.rawSelectedDate = null; + this.displayedDate = null; + this.datePayload = null; + + this.initGlDropdown(); + this.initRemoveDueDate(); + this.initDatePicker(); + } + + initGlDropdown() { + this.$dropdown.glDropdown({ + opened: () => { + const calendar = this.$datePicker.data('pikaday'); + calendar.show(); + }, + hidden: () => { + this.$selectbox.hide(); + this.$value.css('display', ''); + } + }); + } + + initDatePicker() { + const $dueDateInput = $(`input[name='${this.fieldName}']`); + + const calendar = new Pikaday({ + field: $dueDateInput.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect: (dateText) => { + const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); + + $dueDateInput.val(formattedDate); + + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); + this.updateIssueBoardIssue(); + } else { + this.saveDueDate(true); + } + } + }); + + calendar.setDate(new Date($dueDateInput.val())); + this.$datePicker.append(calendar.el); + this.$datePicker.data('pikaday', calendar); + } + + initRemoveDueDate() { + this.$block.on('click', '.js-remove-due-date', (e) => { + const calendar = this.$datePicker.data('pikaday'); + e.preventDefault(); + + calendar.setDate(null); + + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + this.updateIssueBoardIssue(); + } else { + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); + } + }); + } + + saveDueDate(isDropdown) { + this.parseSelectedDate(); + this.prepSelectedDate(); + this.submitSelectedDate(isDropdown); + } + + parseSelectedDate() { + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + + if (this.rawSelectedDate.length) { + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); + this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); + } else { + this.displayedDate = 'No due date'; + } + } + + prepSelectedDate() { + const datePayload = {}; + datePayload[this.abilityName] = {}; + datePayload[this.abilityName].due_date = this.rawSelectedDate; + this.datePayload = datePayload; + } + + updateIssueBoardIssue () { + this.$loading.fadeIn(); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + this.$value.css('display', ''); + + gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) + .then(() => { + this.$loading.fadeOut(); + }); + } + + submitSelectedDate(isDropdown) { + return $.ajax({ + type: 'PUT', + url: this.issueUpdateURL, + data: this.datePayload, + dataType: 'json', + beforeSend: () => { + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + + this.$loading.removeClass('hidden').fadeIn(); + + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } + + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); + + return selectedDateValue.length ? + $('.js-remove-due-date-holder').removeClass('hidden') : + $('.js-remove-due-date-holder').addClass('hidden'); + } + }).done((data) => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); + } +} + +class DueDateSelectors { + constructor() { + this.initMilestoneDatePicker(); + this.initIssuableSelect(); + } + + initMilestoneDatePicker() { + $('.datepicker').each(function() { + const $datePicker = $(this); + const calendar = new Pikaday({ + field: $datePicker.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect(dateText) { + $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } + }); + calendar.setDate(new Date($datePicker.val())); + + $datePicker.data('pikaday', calendar); + }); + + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { + e.preventDefault(); + const calendar = $(e.target).siblings('.datepicker').data('pikaday'); + calendar.setDate(null); + }); + } + + initIssuableSelect() { + const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + + $('.js-due-date-select').each((i, dropdown) => { + const $dropdown = $(dropdown); + new DueDateSelect({ + $dropdown, + $loading + }); + }); + } +} + +window.gl = window.gl || {}; +window.gl.DueDateSelectors = DueDateSelectors; diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 deleted file mode 100644 index 9169fcd7328..00000000000 --- a/app/assets/javascripts/due_date_select.js.es6 +++ /dev/null @@ -1,204 +0,0 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ -/* global dateFormat */ -/* global Pikaday */ - -(function(global) { - class DueDateSelect { - constructor({ $dropdown, $loading } = {}) { - const $dropdownParent = $dropdown.closest('.dropdown'); - const $block = $dropdown.closest('.block'); - this.$loading = $loading; - this.$dropdown = $dropdown; - this.$dropdownParent = $dropdownParent; - this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); - this.$block = $block; - this.$selectbox = $dropdown.closest('.selectbox'); - this.$value = $block.find('.value'); - this.$valueContent = $block.find('.value-content'); - this.$sidebarValue = $('.js-due-date-sidebar-value', $block); - this.fieldName = $dropdown.data('field-name'), - this.abilityName = $dropdown.data('ability-name'), - this.issueUpdateURL = $dropdown.data('issue-update'); - - this.rawSelectedDate = null; - this.displayedDate = null; - this.datePayload = null; - - this.initGlDropdown(); - this.initRemoveDueDate(); - this.initDatePicker(); - } - - initGlDropdown() { - this.$dropdown.glDropdown({ - opened: () => { - const calendar = this.$datePicker.data('pikaday'); - calendar.show(); - }, - hidden: () => { - this.$selectbox.hide(); - this.$value.css('display', ''); - } - }); - } - - initDatePicker() { - const $dueDateInput = $(`input[name='${this.fieldName}']`); - - const calendar = new Pikaday({ - field: $dueDateInput.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect: (dateText) => { - const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - - $dueDateInput.val(formattedDate); - - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); - this.updateIssueBoardIssue(); - } else { - this.saveDueDate(true); - } - } - }); - - calendar.setDate(new Date($dueDateInput.val())); - this.$datePicker.append(calendar.el); - this.$datePicker.data('pikaday', calendar); - } - - initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { - const calendar = this.$datePicker.data('pikaday'); - e.preventDefault(); - - calendar.setDate(null); - - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; - this.updateIssueBoardIssue(); - } else { - $("input[name='" + this.fieldName + "']").val(''); - return this.saveDueDate(false); - } - }); - } - - saveDueDate(isDropdown) { - this.parseSelectedDate(); - this.prepSelectedDate(); - this.submitSelectedDate(isDropdown); - } - - parseSelectedDate() { - this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); - - if (this.rawSelectedDate.length) { - // Construct Date object manually to avoid buggy dateString support within Date constructor - const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); - const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); - this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); - } else { - this.displayedDate = 'No due date'; - } - } - - prepSelectedDate() { - const datePayload = {}; - datePayload[this.abilityName] = {}; - datePayload[this.abilityName].due_date = this.rawSelectedDate; - this.datePayload = datePayload; - } - - updateIssueBoardIssue () { - this.$loading.fadeIn(); - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - this.$value.css('display', ''); - - gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) - .then(() => { - this.$loading.fadeOut(); - }); - } - - submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - - this.$loading.fadeIn(); - - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } - - this.$value.css('display', ''); - this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); - this.$sidebarValue.html(this.displayedDate); - - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - } - }).done((data) => { - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - return this.$loading.fadeOut(); - }); - } - } - - class DueDateSelectors { - constructor() { - this.initMilestoneDatePicker(); - this.initIssuableSelect(); - } - - initMilestoneDatePicker() { - $('.datepicker').each(function() { - const $datePicker = $(this); - const calendar = new Pikaday({ - field: $datePicker.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect(dateText) { - $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - } - }); - calendar.setDate(new Date($datePicker.val())); - - $datePicker.data('pikaday', calendar); - }); - - $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { - e.preventDefault(); - const calendar = $(e.target).siblings('.datepicker').data('pikaday'); - calendar.setDate(null); - }); - } - - initIssuableSelect() { - const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); - - $('.js-due-date-select').each((i, dropdown) => { - const $dropdown = $(dropdown); - new DueDateSelect({ - $dropdown, - $loading - }); - }); - } - } - - global.DueDateSelectors = DueDateSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js new file mode 100644 index 00000000000..51aab8460f6 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment.js @@ -0,0 +1,192 @@ +/* eslint-disable no-new */ +/* global Flash */ +import Vue from 'vue'; +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from './environments_table'; +import EnvironmentsStore from '../stores/environments_store'; +import TablePaginationComponent from '../../vue_shared/components/table_pagination'; +import '../../lib/utils/common_utils'; +import eventHub from '../event_hub'; + +export default Vue.component('environment-component', { + + components: { + 'environment-table': EnvironmentTable, + 'table-pagination': TablePaginationComponent, + }, + + data() { + const environmentsData = document.querySelector('#environments-list-view').dataset; + const store = new EnvironmentsStore(); + + return { + store, + state: store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + endpoint: environmentsData.environmentsDataEndpoint, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + canCreateEnvironment: environmentsData.canCreateEnvironment, + projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, + projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, + newEnvironmentPath: environmentsData.newEnvironmentPath, + helpPagePath: environmentsData.helpPagePath, + + // Pagination Properties, + paginationInformation: {}, + pageNumber: 1, + }; + }, + + computed: { + scope() { + return gl.utils.getParameterByName('scope'); + }, + + canReadEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + }, + + canCreateEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); + }, + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + this.service = new EnvironmentsService(this.endpoint); + + this.fetchEnvironments(); + + eventHub.$on('refreshEnvironments', this.fetchEnvironments); + }, + + beforeDestroyed() { + eventHub.$off('refreshEnvironments'); + }, + + methods: { + toggleRow(model) { + return this.store.toggleFolder(model.name); + }, + + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + * @return {String} + */ + changePage(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + + fetchEnvironments() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + this.isLoading = true; + + return this.service.get(scope, pageNumber) + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.'); + }); + }, + }, + + template: ` + <div :class="cssContainerClass"> + <div class="top-area"> + <ul v-if="!isLoading" class="nav-links"> + <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> + <a :href="projectEnvironmentsPath"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li v-bind:class="{ 'active' : scope === 'stopped' }"> + <a :href="projectStoppedEnvironmentsPath"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> + <a :href="newEnvironmentPath" class="btn btn-create"> + New environment + </a> + </div> + </div> + + <div class="content-list environments-container"> + <div class="environments-list-loading text-center" v-if="isLoading"> + <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.environments.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments + </a> + </p> + + <a v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New Environment + </a> + </div> + + <div class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :service="service"/> + </div> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"> + </table-pagination> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 deleted file mode 100644 index 4b700a39d44..00000000000 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ /dev/null @@ -1,193 +0,0 @@ -/* eslint-disable no-param-reassign, no-new */ -/* global Flash */ - -const Vue = window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('./environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); -require('../../vue_shared/components/table_pagination'); -require('../../lib/utils/common_utils'); -require('../../vue_shared/vue_resource_interceptor'); - -module.exports = Vue.component('environment-component', { - - components: { - 'environment-table': EnvironmentTable, - 'table-pagination': gl.VueGlPagination, - }, - - data() { - const environmentsData = document.querySelector('#environments-list-view').dataset; - const store = new EnvironmentsStore(); - - return { - store, - state: store.state, - visibility: 'available', - isLoading: false, - cssContainerClass: environmentsData.cssClass, - endpoint: environmentsData.environmentsDataEndpoint, - canCreateDeployment: environmentsData.canCreateDeployment, - canReadEnvironment: environmentsData.canReadEnvironment, - canCreateEnvironment: environmentsData.canCreateEnvironment, - projectEnvironmentsPath: environmentsData.projectEnvironmentsPath, - projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, - newEnvironmentPath: environmentsData.newEnvironmentPath, - helpPagePath: environmentsData.helpPagePath, - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, - - // Pagination Properties, - paginationInformation: {}, - pageNumber: 1, - }; - }, - - computed: { - scope() { - return gl.utils.getParameterByName('scope'); - }, - - canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); - }, - - canCreateEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); - }, - - }, - - /** - * Fetches all the environments and stores them. - * Toggles loading property. - */ - created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; - - const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - - const service = new EnvironmentsService(endpoint); - - this.isLoading = true; - - return service.all() - .then(resp => ({ - headers: resp.headers, - body: resp.json(), - })) - .then((response) => { - this.store.storeAvailableCount(response.body.available_count); - this.store.storeStoppedCount(response.body.stopped_count); - this.store.storeEnvironments(response.body.environments); - this.store.setPagination(response.headers); - }) - .then(() => { - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); - }, - - methods: { - toggleRow(model) { - return this.store.toggleFolder(model.name); - }, - - /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. - * @return {String} - */ - changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); - - gl.utils.visitUrl(param); - return param; - }, - }, - - template: ` - <div :class="cssContainerClass"> - <div class="top-area"> - <ul v-if="!isLoading" class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="projectEnvironmentsPath"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="projectStoppedEnvironmentsPath"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> - <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-create"> - New environment - </a> - </div> - </div> - - <div class="environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments - </a> - </p> - - <a v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New Environment - </a> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> - - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - </environment-table> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> - </div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js new file mode 100644 index 00000000000..455a8819549 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -0,0 +1,71 @@ +/* global Flash */ +/* eslint-disable no-new */ + +import playIconSvg from 'icons/_icon_play.svg'; +import eventHub from '../event_hub'; + +export default { + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + playIconSvg, + isLoading: false, + }; + }, + + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + }, + + template: ` + <div class="btn-group" role="group"> + <button + class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + data-toggle="dropdown" + :disabled="isLoading"> + <span> + <span v-html="playIconSvg"></span> + <i class="fa fa-caret-down" aria-hidden="true"></i> + <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </span> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <button + @click="onClickAction(action.play_path)" + class="js-manual-action-link no-btn"> + ${playIconSvg} + <span> + {{action.name}} + </span> + </button> + </li> + </ul> + </button> + </div> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 deleted file mode 100644 index c5a714d9673..00000000000 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -const Vue = require('vue'); - -module.exports = Vue.component('actions-component', { - props: { - actions: { - type: Array, - required: false, - default: () => [], - }, - - playIconSvg: { - type: String, - required: false, - }, - }, - - template: ` - <div class="inline"> - <div class="dropdown"> - <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> - <i class="fa fa-caret-down"></i> - </a> - - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> - <a :href="action.play_path" - data-method="post" - rel="nofollow" - class="js-manual-action-link"> - - <span class="js-action-play-icon-container" v-html="playIconSvg"></span> - - <span> - {{action.name}} - </span> - </a> - </li> - </ul> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js new file mode 100644 index 00000000000..b4f9eb357fd --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -0,0 +1,22 @@ +/** + * Renders the external url link in environments table. + */ +export default { + props: { + externalUrl: { + type: String, + default: '', + }, + }, + + template: ` + <a + class="btn external_url" + :href="externalUrl" + target="_blank" + rel="noopener noreferrer" + title="Environment external URL"> + <i class="fa fa-external-link" aria-hidden="true"></i> + </a> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 deleted file mode 100644 index 2599bba3c59..00000000000 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Renders the external url link in environments table. - */ -const Vue = require('vue'); - -module.exports = Vue.component('external-url-component', { - props: { - externalUrl: { - type: String, - default: '', - }, - }, - - template: ` - <a class="btn external_url" :href="externalUrl" target="_blank"> - <i class="fa fa-external-link"></i> - </a> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js new file mode 100644 index 00000000000..66ed10e19d1 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -0,0 +1,514 @@ +import Timeago from 'timeago.js'; +import '../../lib/utils/text_utility'; +import ActionsComponent from './environment_actions'; +import ExternalUrlComponent from './environment_external_url'; +import StopComponent from './environment_stop'; +import RollbackComponent from './environment_rollback'; +import TerminalButtonComponent from './environment_terminal_button'; +import CommitComponent from '../../vue_shared/components/commit'; + +/** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ +const timeagoInstance = new Timeago(); + +export default { + components: { + 'commit-component': CommitComponent, + 'actions-component': ActionsComponent, + 'external-url-component': ExternalUrlComponent, + 'stop-component': StopComponent, + 'rollback-component': RollbackComponent, + 'terminal-button-component': TerminalButtonComponent, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + service: { + type: Object, + required: true, + }, + }, + + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && + this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model && this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_path: action.play_path, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.model.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.model.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + /** + * Constructs folder URL based on the current location and the folder id. + * + * @return {String} + */ + folderUrl() { + return `${window.location.pathname}/folders/${this.model.folderName}`; + }, + + }, + + /** + * Helper to verify if certain given object are empty. + * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty + * @param {Object} object + * @returns {Bollean} + */ + isObjectEmpty(object) { + for (const key in object) { // eslint-disable-line + if (hasOwnProperty.call(object, key)) { + return false; + } + } + return true; + }, + + template: ` + <tr> + <td> + <a v-if="!model.isFolder" + class="environment-name" + :href="environmentPath"> + {{model.name}} + </a> + <a v-else class="folder-name" :href="folderUrl"> + <span class="folder-icon"> + <i class="fa fa-folder" aria-hidden="true"></i> + </span> + + <span> + {{model.folderName}} + </span> + + <span class="badge"> + {{model.size}} + </span> + </a> + </td> + + <td class="deployment-column"> + <span v-if="shouldRenderDeploymentID"> + {{deploymentInternalId}} + </span> + + <span v-if="!model.isFolder && deploymentHasUser"> + by + <a :href="deploymentUser.web_url" class="js-deploy-user-container"> + <img class="avatar has-tooltip s20" + :src="deploymentUser.avatar_url" + :alt="userImageAltDescription" + :title="deploymentUser.username" /> + </a> + </span> + </td> + + <td class="environments-build-cell"> + <a v-if="shouldRenderBuildName" + class="build-link" + :href="buildPath"> + {{buildName}} + </a> + </td> + + <td> + <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component"> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor"/> + </div> + <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title"> + No deployments yet + </p> + </td> + + <td> + <span v-if="!model.isFolder && canShowDate" + class="environment-created-date-timeago"> + {{createdDate}} + </span> + </td> + + <td class="environments-actions"> + <div v-if="!model.isFolder" class="btn-group pull-right" role="group"> + <actions-component v-if="hasManualActions && canCreateDeployment" + :service="service" + :actions="manualActions"/> + + <external-url-component v-if="externalURL && canReadEnvironment" + :external-url="externalURL"/> + + <stop-component v-if="hasStopAction && canCreateDeployment" + :stop-url="model.stop_path" + :service="service"/> + + <terminal-button-component v-if="model && model.terminal_path" + :terminal-path="model.terminal_path"/> + + <rollback-component v-if="canRetry && canCreateDeployment" + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl" + :service="service"/> + </div> + </td> + </tr> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 deleted file mode 100644 index 24fd58a301a..00000000000 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ /dev/null @@ -1,549 +0,0 @@ -const Vue = require('vue'); -const Timeago = require('timeago.js'); - -require('../../lib/utils/text_utility'); -require('../../vue_shared/components/commit'); -const ActionsComponent = require('./environment_actions'); -const ExternalUrlComponent = require('./environment_external_url'); -const StopComponent = require('./environment_stop'); -const RollbackComponent = require('./environment_rollback'); -const TerminalButtonComponent = require('./environment_terminal_button'); - -/** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ - -const timeagoInstance = new Timeago(); - -module.exports = Vue.component('environment-item', { - - components: { - 'commit-component': gl.CommitComponent, - 'actions-component': ActionsComponent, - 'external-url-component': ExternalUrlComponent, - 'stop-component': StopComponent, - 'rollback-component': RollbackComponent, - 'terminal-button-component': TerminalButtonComponent, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, - }, - - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !this.$options.isObjectEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: gl.text.humanize(action.name), - play_path: action.play_path, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - !this.$options.isObjectEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !this.$options.isObjectEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - /** - * Constructs folder URL based on the current location and the folder id. - * - * @return {String} - */ - folderUrl() { - return `${window.location.pathname}/folders/${this.model.folderName}`; - }, - - }, - - /** - * Helper to verify if certain given object are empty. - * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty - * @param {Object} object - * @returns {Bollean} - */ - isObjectEmpty(object) { - for (const key in object) { // eslint-disable-line - if (hasOwnProperty.call(object, key)) { - return false; - } - } - return true; - }, - - template: ` - <tr> - <td> - <a v-if="!model.isFolder" - class="environment-name" - :href="environmentPath"> - {{model.name}} - </a> - <a v-else class="folder-name" :href="folderUrl"> - <span class="folder-icon"> - <i class="fa fa-folder" aria-hidden="true"></i> - </span> - - <span> - {{model.folderName}} - </span> - - <span class="badge"> - {{model.size}} - </span> - </a> - </td> - - <td class="deployment-column"> - <span v-if="shouldRenderDeploymentID"> - {{deploymentInternalId}} - </span> - - <span v-if="!model.isFolder && deploymentHasUser"> - by - <a :href="deploymentUser.web_url" class="js-deploy-user-container"> - <img class="avatar has-tooltip s20" - :src="deploymentUser.avatar_url" - :alt="userImageAltDescription" - :title="deploymentUser.username" /> - </a> - </span> - </td> - - <td class="environments-build-cell"> - <a v-if="shouldRenderBuildName" - class="build-link" - :href="buildPath"> - {{buildName}} - </a> - </td> - - <td> - <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component"> - <commit-component - :tag="commitTag" - :commit-ref="commitRef" - :commit-url="commitUrl" - :short-sha="commitShortSha" - :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="commitIconSvg"> - </commit-component> - </div> - <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title"> - No deployments yet - </p> - </td> - - <td> - <span v-if="!model.isFolder && canShowDate" - class="environment-created-date-timeago"> - {{createdDate}} - </span> - </td> - - <td class="hidden-xs"> - <div v-if="!model.isFolder"> - <div v-if="hasManualActions && canCreateDeployment" - class="inline js-manual-actions-container"> - <actions-component - :play-icon-svg="playIconSvg" - :actions="manualActions"> - </actions-component> - </div> - - <div v-if="externalURL && canReadEnvironment" - class="inline js-external-url-container"> - <external-url-component - :external-url="externalURL"> - </external-url-component> - </div> - - <div v-if="hasStopAction && canCreateDeployment" - class="inline js-stop-component-container"> - <stop-component - :stop-url="model.stop_path"> - </stop-component> - </div> - - <div v-if="model && model.terminal_path" - class="inline js-terminal-button-container"> - <terminal-button-component - :terminal-icon-svg="terminalIconSvg" - :terminal-path="model.terminal_path"> - </terminal-button-component> - </div> - - <div v-if="canRetry && canCreateDeployment" - class="inline js-rollback-component-container"> - <rollback-component - :is-last-deployment="isLastDeployment" - :retry-url="retryUrl"> - </rollback-component> - </div> - </div> - </td> - </tr> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js new file mode 100644 index 00000000000..baa15d9e5b5 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_rollback.js @@ -0,0 +1,67 @@ +/* global Flash */ +/* eslint-disable no-new */ +/** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ +import eventHub from '../event_hub'; + +export default { + props: { + retryUrl: { + type: String, + default: '', + }, + + isLastDeployment: { + type: Boolean, + default: true, + }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + }, + + template: ` + <button type="button" + class="btn" + @click="onClick" + :disabled="isLoading"> + + <span v-if="isLastDeployment"> + Re-deploy + </span> + <span v-else> + Rollback + </span> + + <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </button> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 deleted file mode 100644 index daf126eb4e8..00000000000 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment` - */ -const Vue = require('vue'); - -module.exports = Vue.component('rollback-component', { - props: { - retryUrl: { - type: String, - default: '', - }, - - isLastDeployment: { - type: Boolean, - default: true, - }, - }, - - template: ` - <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> - <span v-if="isLastDeployment"> - Re-deploy - </span> - <span v-else> - Rollback - </span> - </a> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js new file mode 100644 index 00000000000..5404d647745 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -0,0 +1,56 @@ +/* global Flash */ +/* eslint-disable no-new, no-alert */ +/** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ +import eventHub from '../event_hub'; + +export default { + props: { + stopUrl: { + type: String, + default: '', + }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.', 'alert'); + }); + } + }, + }, + + template: ` + <button type="button" + class="btn stop-env-link" + @click="onClick" + :disabled="isLoading" + title="Stop Environment"> + <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> + <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </button> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 deleted file mode 100644 index 96983a19568..00000000000 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ -const Vue = require('vue'); - -module.exports = Vue.component('stop-component', { - props: { - stopUrl: { - type: String, - default: '', - }, - }, - - template: ` - <a class="btn stop-env-link" - :href="stopUrl" - data-confirm="Are you sure you want to stop this environment?" - data-method="post" - rel="nofollow"> - <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> - </a> - `, -}); diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js new file mode 100644 index 00000000000..66a71faa02f --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -0,0 +1,27 @@ +/** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ +import terminalIconSvg from 'icons/_icon_terminal.svg'; + +export default { + props: { + terminalPath: { + type: String, + required: false, + default: '', + }, + }, + + data() { + return { terminalIconSvg }; + }, + + template: ` + <a class="btn terminal-button" + title="Open web terminal" + :href="terminalPath"> + ${terminalIconSvg} + </a> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 deleted file mode 100644 index 481e0d15e7a..00000000000 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ -const Vue = require('vue'); - -module.exports = Vue.component('terminal-button-component', { - props: { - terminalPath: { - type: String, - default: '', - }, - terminalIconSvg: { - type: String, - default: '', - }, - }, - - template: ` - <a class="btn terminal-button" - :href="terminalPath"> - <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> - </a> - `, -}); diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js new file mode 100644 index 00000000000..338dff40bc9 --- /dev/null +++ b/app/assets/javascripts/environments/components/environments_table.js @@ -0,0 +1,60 @@ +/** + * Render environments table. + */ +import EnvironmentTableRowComponent from './environment_item'; + +export default { + components: { + 'environment-item': EnvironmentTableRowComponent, + }, + + props: { + environments: { + type: Array, + required: true, + default: () => ([]), + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + service: { + type: Object, + required: true, + }, + }, + + template: ` + <table class="table ci-table"> + <thead> + <tr> + <th class="environments-name">Environment</th> + <th class="environments-deploy">Last deployment</th> + <th class="environments-build">Job</th> + <th class="environments-commit">Commit</th> + <th class="environments-date">Updated</th> + <th class="environments-actions"></th> + </tr> + </thead> + <tbody> + <template v-for="model in environments" + v-bind:model="model"> + <tr is="environment-item" + :model="model" + :can-create-deployment="canCreateDeployment" + :can-read-environment="canReadEnvironment" + :service="service"></tr> + </template> + </tbody> + </table> + `, +}; diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 deleted file mode 100644 index fd35d77fd3d..00000000000 --- a/app/assets/javascripts/environments/components/environments_table.js.es6 +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Render environments table. - */ -const Vue = require('vue'); -const EnvironmentItem = require('./environment_item'); - -module.exports = Vue.component('environment-table-component', { - - components: { - 'environment-item': EnvironmentItem, - }, - - props: { - environments: { - type: Array, - required: true, - default: () => ([]), - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - commitIconSvg: { - type: String, - required: false, - }, - - playIconSvg: { - type: String, - required: false, - }, - - terminalIconSvg: { - type: String, - required: false, - }, - }, - - template: ` - <table class="table ci-table environments"> - <thead> - <tr> - <th class="environments-name">Environment</th> - <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Job</th> - <th class="environments-commit">Commit</th> - <th class="environments-date">Updated</th> - <th class="hidden-xs environments-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in environments" - v-bind:model="model"> - <tr is="environment-item" - :model="model" - :can-create-deployment="canCreateDeployment" - :can-read-environment="canReadEnvironment" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"></tr> - </template> - </tbody> - </table> - `, -}); diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js new file mode 100644 index 00000000000..8d963b335cf --- /dev/null +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -0,0 +1,13 @@ +import EnvironmentsComponent from './components/environment'; + +$(() => { + window.gl = window.gl || {}; + + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); + } + + gl.EnvironmentsListApp = new EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + }); +}); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 deleted file mode 100644 index 7bbba91bc10..00000000000 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -const EnvironmentsComponent = require('./components/environment'); - -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListApp) { - gl.EnvironmentsListApp.$destroy(true); - } - - gl.EnvironmentsListApp = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); -}); diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/environments/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js new file mode 100644 index 00000000000..f939eccf246 --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -0,0 +1,13 @@ +import EnvironmentsFolderComponent from './environments_folder_view'; + +$(() => { + window.gl = window.gl || {}; + + if (gl.EnvironmentsListFolderApp) { + gl.EnvironmentsListFolderApp.$destroy(true); + } + + gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({ + el: document.querySelector('#environments-folder-list-view'), + }); +}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 deleted file mode 100644 index d2ca465351a..00000000000 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -const EnvironmentsFolderComponent = require('./environments_folder_view'); - -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListFolderApp) { - gl.EnvironmentsListFolderApp.$destroy(true); - } - - gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({ - el: document.querySelector('#environments-folder-list-view'), - }); -}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js new file mode 100644 index 00000000000..8abbcf0c227 --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js @@ -0,0 +1,178 @@ +/* eslint-disable no-new */ +/* global Flash */ +import Vue from 'vue'; +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from '../components/environments_table'; +import EnvironmentsStore from '../stores/environments_store'; +import TablePaginationComponent from '../../vue_shared/components/table_pagination'; +import '../../lib/utils/common_utils'; +import '../../vue_shared/vue_resource_interceptor'; + +export default Vue.component('environment-folder-view', { + components: { + 'environment-table': EnvironmentTable, + 'table-pagination': TablePaginationComponent, + }, + + data() { + const environmentsData = document.querySelector('#environments-folder-list-view').dataset; + const store = new EnvironmentsStore(); + const pathname = window.location.pathname; + const endpoint = `${pathname}.json`; + const folderName = pathname.substr(pathname.lastIndexOf('/') + 1); + + return { + store, + folderName, + endpoint, + state: store.state, + visibility: 'available', + isLoading: false, + cssContainerClass: environmentsData.cssClass, + canCreateDeployment: environmentsData.canCreateDeployment, + canReadEnvironment: environmentsData.canReadEnvironment, + + // svgs + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, + + // Pagination Properties, + paginationInformation: {}, + pageNumber: 1, + }; + }, + + computed: { + scope() { + return gl.utils.getParameterByName('scope'); + }, + + canReadEnvironmentParsed() { + return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + }, + + /** + * URL to link in the stopped tab. + * + * @return {String} + */ + stoppedPath() { + return `${window.location.pathname}?scope=stopped`; + }, + + /** + * URL to link in the available tab. + * + * @return {String} + */ + availablePath() { + return window.location.pathname; + }, + }, + + /** + * Fetches all the environments and stores them. + * Toggles loading property. + */ + created() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; + + this.service = new EnvironmentsService(endpoint); + + this.isLoading = true; + + return this.service.get() + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); + }); + }, + + methods: { + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + */ + changePage(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + }, + + template: ` + <div :class="cssContainerClass"> + <div class="top-area" v-if="!isLoading"> + + <h4 class="js-folder-name environments-folder-name"> + Environments / <b>{{folderName}}</b> + </h4> + + <ul class="nav-links"> + <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> + <a :href="availablePath" class="js-available-environments-folder-tab"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li v-bind:class="{ 'active' : scope === 'stopped' }"> + <a :href="stoppedPath" class="js-stopped-environments-folder-tab"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + </div> + + <div class="environments-container"> + <div class="environments-list-loading text-center" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" + :commit-icon-svg="commitIconSvg" + :service="service"/> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"/> + </div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 deleted file mode 100644 index 53d52965758..00000000000 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 +++ /dev/null @@ -1,182 +0,0 @@ -/* eslint-disable no-param-reassign, no-new */ -/* global Flash */ - -const Vue = window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('../components/environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); -require('../../vue_shared/components/table_pagination'); -require('../../lib/utils/common_utils'); -require('../../vue_shared/vue_resource_interceptor'); - -module.exports = Vue.component('environment-folder-view', { - - components: { - 'environment-table': EnvironmentTable, - 'table-pagination': gl.VueGlPagination, - }, - - data() { - const environmentsData = document.querySelector('#environments-folder-list-view').dataset; - const store = new EnvironmentsStore(); - const pathname = window.location.pathname; - const endpoint = `${pathname}.json`; - const folderName = pathname.substr(pathname.lastIndexOf('/') + 1); - - return { - store, - folderName, - endpoint, - state: store.state, - visibility: 'available', - isLoading: false, - cssContainerClass: environmentsData.cssClass, - canCreateDeployment: environmentsData.canCreateDeployment, - canReadEnvironment: environmentsData.canReadEnvironment, - - // svgs - commitIconSvg: environmentsData.commitIconSvg, - playIconSvg: environmentsData.playIconSvg, - terminalIconSvg: environmentsData.terminalIconSvg, - - // Pagination Properties, - paginationInformation: {}, - pageNumber: 1, - }; - }, - - computed: { - scope() { - return gl.utils.getParameterByName('scope'); - }, - - canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); - }, - - /** - * URL to link in the stopped tab. - * - * @return {String} - */ - stoppedPath() { - return `${window.location.pathname}?scope=stopped`; - }, - - /** - * URL to link in the available tab. - * - * @return {String} - */ - availablePath() { - return window.location.pathname; - }, - }, - - /** - * Fetches all the environments and stores them. - * Toggles loading property. - */ - created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; - - const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - - const service = new EnvironmentsService(endpoint); - - this.isLoading = true; - - return service.all() - .then(resp => ({ - headers: resp.headers, - body: resp.json(), - })) - .then((response) => { - this.store.storeAvailableCount(response.body.available_count); - this.store.storeStoppedCount(response.body.stopped_count); - this.store.storeEnvironments(response.body.environments); - this.store.setPagination(response.headers); - }) - .then(() => { - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); - }, - - methods: { - /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. - */ - changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); - - gl.utils.visitUrl(param); - return param; - }, - }, - - template: ` - <div :class="cssContainerClass"> - <div class="top-area" v-if="!isLoading"> - - <h4 class="js-folder-name environments-folder-name"> - Environments / <b>{{folderName}}</b> - </h4> - - <ul class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="availablePath" class="js-available-environments-folder-tab"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="stoppedPath" class="js-stopped-environments-folder-tab"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> - </div> - - <div class="environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> - - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :play-icon-svg="playIconSvg" - :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - </environment-table> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> - </div> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js new file mode 100644 index 00000000000..07040bf0d73 --- /dev/null +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -0,0 +1,19 @@ +/* eslint-disable class-methods-use-this */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class EnvironmentsService { + constructor(endpoint) { + this.environments = Vue.resource(endpoint); + } + + get(scope, page) { + return this.environments.get({ scope, page }); + } + + postAction(endpoint) { + return Vue.http.post(endpoint, {}, { emulateJSON: true }); + } +} diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 deleted file mode 100644 index 9cef335868e..00000000000 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -const Vue = require('vue'); - -class EnvironmentsService { - constructor(endpoint) { - this.environments = Vue.resource(endpoint); - } - - all() { - return this.environments.get(); - } -} - -module.exports = EnvironmentsService; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js new file mode 100644 index 00000000000..3c3084f3b78 --- /dev/null +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -0,0 +1,88 @@ +import '~/lib/utils/common_utils'; +/** + * Environments Store. + * + * Stores received environments, count of stopped environments and count of + * available environments. + */ +export default class EnvironmentsStore { + constructor() { + this.state = {}; + this.state.environments = []; + this.state.stoppedCounter = 0; + this.state.availableCounter = 0; + this.state.paginationInformation = {}; + + return this; + } + + /** + * + * Stores the received environments. + * + * In the main environments endpoint, each environment has the following schema + * { name: String, size: Number, latest: Object } + * In the endpoint to retrieve environments from each folder, the environment does + * not have the `latest` key and the data is all in the root level. + * To avoid doing this check in the view, we store both cases the same by extracting + * what is inside the `latest` key. + * + * If the `size` is bigger than 1, it means it should be rendered as a folder. + * In those cases we add `isFolder` key in order to render it properly. + * + * @param {Array} environments + * @returns {Array} + */ + storeEnvironments(environments = []) { + const filteredEnvironments = environments.map((env) => { + let filtered = {}; + + if (env.size > 1) { + filtered = Object.assign({}, env, { isFolder: true, folderName: env.name }); + } + + if (env.latest) { + filtered = Object.assign(filtered, env, env.latest); + delete filtered.latest; + } else { + filtered = Object.assign(filtered, env); + } + + return filtered; + }); + + this.state.environments = filteredEnvironments; + + return filteredEnvironments; + } + + setPagination(pagination = {}) { + const normalizedHeaders = gl.utils.normalizeHeaders(pagination); + const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); + + this.state.paginationInformation = paginationInformation; + return paginationInformation; + } + + /** + * Stores the number of available environments. + * + * @param {Number} count = 0 + * @return {Number} + */ + storeAvailableCount(count = 0) { + this.state.availableCounter = count; + return count; + } + + /** + * Stores the number of closed environments. + * + * @param {Number} count = 0 + * @return {Number} + */ + storeStoppedCount(count = 0) { + this.state.stoppedCounter = count; + return count; + } +} diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 deleted file mode 100644 index 15cd9bde08e..00000000000 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ /dev/null @@ -1,90 +0,0 @@ -require('~/lib/utils/common_utils'); -/** - * Environments Store. - * - * Stores received environments, count of stopped environments and count of - * available environments. - */ -class EnvironmentsStore { - constructor() { - this.state = {}; - this.state.environments = []; - this.state.stoppedCounter = 0; - this.state.availableCounter = 0; - this.state.paginationInformation = {}; - - return this; - } - - /** - * - * Stores the received environments. - * - * In the main environments endpoint, each environment has the following schema - * { name: String, size: Number, latest: Object } - * In the endpoint to retrieve environments from each folder, the environment does - * not have the `latest` key and the data is all in the root level. - * To avoid doing this check in the view, we store both cases the same by extracting - * what is inside the `latest` key. - * - * If the `size` is bigger than 1, it means it should be rendered as a folder. - * In those cases we add `isFolder` key in order to render it properly. - * - * @param {Array} environments - * @returns {Array} - */ - storeEnvironments(environments = []) { - const filteredEnvironments = environments.map((env) => { - let filtered = {}; - - if (env.size > 1) { - filtered = Object.assign({}, env, { isFolder: true, folderName: env.name }); - } - - if (env.latest) { - filtered = Object.assign(filtered, env, env.latest); - delete filtered.latest; - } else { - filtered = Object.assign(filtered, env); - } - - return filtered; - }); - - this.state.environments = filteredEnvironments; - - return filteredEnvironments; - } - - setPagination(pagination = {}) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); - - this.state.paginationInformation = paginationInformation; - return paginationInformation; - } - - /** - * Stores the number of available environments. - * - * @param {Number} count = 0 - * @return {Number} - */ - storeAvailableCount(count = 0) { - this.state.availableCounter = count; - return count; - } - - /** - * Stores the number of closed environments. - * - * @param {Number} count = 0 - * @return {Number} - */ - storeStoppedCount(count = 0) { - this.state.stoppedCounter = count; - return count; - } -} - -module.exports = EnvironmentsStore; diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js new file mode 100644 index 00000000000..027222f804d --- /dev/null +++ b/app/assets/javascripts/extensions/array.js @@ -0,0 +1,11 @@ +// TODO: remove this + +// eslint-disable-next-line no-extend-native +Array.prototype.first = function first() { + return this[0]; +}; + +// eslint-disable-next-line no-extend-native +Array.prototype.last = function last() { + return this[this.length - 1]; +}; diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 deleted file mode 100644 index f8256a8d26d..00000000000 --- a/app/assets/javascripts/extensions/array.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ - -'use strict'; - -Array.prototype.first = function() { - return this[0]; -}; - -Array.prototype.last = function() { - return this[this.length-1]; -}; - -Array.prototype.find = Array.prototype.find || function(predicate, ...args) { - if (!this) throw new TypeError('Array.prototype.find called on null or undefined'); - if (typeof predicate !== 'function') throw new TypeError('predicate must be a function'); - - const list = Object(this); - const thisArg = args[1]; - let value = {}; - - for (let i = 0; i < list.length; i += 1) { - value = list[i]; - if (predicate.call(thisArg, value, i, list)) return value; - } - - return undefined; -}; diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 deleted file mode 100644 index abedae4c1c7..00000000000 --- a/app/assets/javascripts/extensions/custom_event.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* global CustomEvent */ -/* eslint-disable no-global-assign */ - -// Custom event support for IE -CustomEvent = function CustomEvent(event, parameters) { - const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; -}; - -CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 deleted file mode 100644 index 90ab79305a7..00000000000 --- a/app/assets/javascripts/extensions/element.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -/* global Element */ -/* eslint-disable consistent-return, max-len, no-empty, func-names */ - -Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { - if (!selectedElement) return; - return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); -}; - -Element.prototype.matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector || - function (s) { - const matches = (this.document || this.ownerDocument).querySelectorAll(s); - let i = matches.length - 1; - while (i >= 0 && matches.item(i) !== this) { i -= 1; } - return i > -1; - }; diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js deleted file mode 100644 index 1a489b859e8..00000000000 --- a/app/assets/javascripts/extensions/jquery.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */ -// Disable an element and add the 'disabled' Bootstrap class -(function() { - $.fn.extend({ - disable: function() { - return $(this).attr('disabled', 'disabled').addClass('disabled'); - } - }); - - // Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend({ - enable: function() { - return $(this).removeAttr('disabled').removeClass('disabled'); - } - }); -}).call(window); diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 deleted file mode 100644 index 70a2d765abd..00000000000 --- a/app/assets/javascripts/extensions/object.js.es6 +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-restricted-syntax */ - -// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -if (typeof Object.assign !== 'function') { - Object.assign = function assign(target, ...args) { - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - const to = Object(target); - - for (let index = 0; index < args.length; index += 1) { - const nextSource = args[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (const nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }; -} diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 698870d0ce1..3f041172ff3 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,147 +1,141 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* global FilesCommentButton */ +/* global notes */ -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +let $commentButtonTemplate; +var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - this.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; +window.FilesCommentButton = (function() { + var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - COMMENT_BUTTON_CLASS = '.add-diff-note'; + COMMENT_BUTTON_CLASS = '.add-diff-note'; - COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + LINE_HOLDER_CLASS = '.line_holder'; - LINE_HOLDER_CLASS = '.line_holder'; + LINE_NUMBER_CLASS = 'diff-line-num'; - LINE_NUMBER_CLASS = 'diff-line-num'; + LINE_CONTENT_CLASS = 'line_content'; - LINE_CONTENT_CLASS = 'line_content'; + UNFOLDABLE_LINE_CLASS = 'js-unfold'; - UNFOLDABLE_LINE_CLASS = 'js-unfold'; + EMPTY_CELL_CLASS = 'empty-cell'; - EMPTY_CELL_CLASS = 'empty-cell'; + OLD_LINE_CLASS = 'old_line'; - OLD_LINE_CLASS = 'old_line'; + LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; + TEXT_FILE_SELECTOR = '.text-file'; - TEXT_FILE_SELECTOR = '.text-file'; + function FilesCommentButton(filesContainerElement) { + this.render = bind(this.render, this); + this.hideButton = bind(this.hideButton, this); + this.isParallelView = notes.isParallelView(); + filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) + .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); + } - DEBOUNCE_TIMEOUT_DURATION = 100; + FilesCommentButton.prototype.render = function(e) { + var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; + $currentTarget = $(e.currentTarget); - function FilesCommentButton(filesContainerElement) { - var debounce; - this.filesContainerElement = filesContainerElement; - this.destroy = bind(this.destroy, this); - this.render = bind(this.render, this); - this.VIEW_TYPE = $('input#view[type=hidden]').val(); - debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); - $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); + if ($currentTarget.hasClass('js-no-comment-btn')) return; + + lineContentElement = this.getLineContent($currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); + + if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + + $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); + buttonParentElement.addClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + + if ($button.length) { + return; } - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement; - $currentTarget = $(e.currentTarget); - - buttonParentElement = this.getButtonParent($currentTarget); - if (!this.validateButtonParent(buttonParentElement)) return; - lineContentElement = this.getLineContent($currentTarget); - if (!this.validateLineContent(lineContentElement)) return; - - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - position: lineContentElement.attr('data-position'), - lineType: lineContentElement.attr('data-line-type'), - discussionID: lineContentElement.attr('data-discussion-id'), - lineCode: lineContentElement.attr('data-line-code') - })); - }; - - FilesCommentButton.prototype.destroy = function(e) { - if (this.isMovingToSameType(e)) { - return; - } - $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - var initializedButtonTemplate; - initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({ - COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1) - }); - return $(initializedButtonTemplate).attr({ - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - 'data-line-code': buttonAttributes.lineCode, - 'data-position': buttonAttributes.position, - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + textFileElement = this.getTextFileElement($currentTarget); + buttonParentElement.append(this.buildButton({ + noteableType: textFileElement.attr('data-noteable-type'), + noteableID: textFileElement.attr('data-noteable-id'), + commitID: textFileElement.attr('data-commit-id'), + noteType: lineContentElement.attr('data-note-type'), + position: lineContentElement.attr('data-position'), + lineType: lineContentElement.attr('data-line-type'), + discussionID: lineContentElement.attr('data-discussion-id'), + lineCode: lineContentElement.attr('data-line-code') + })); + }; + + FilesCommentButton.prototype.hideButton = function(e) { + var $currentTarget = $(e.currentTarget); + var buttonParentElement = this.getButtonParent($currentTarget); + + buttonParentElement.removeClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); + }; + + FilesCommentButton.prototype.buildButton = function(buttonAttributes) { + return $commentButtonTemplate.clone().attr({ + 'data-noteable-type': buttonAttributes.noteableType, + 'data-noteable-id': buttonAttributes.noteableID, + 'data-commit-id': buttonAttributes.commitID, + 'data-note-type': buttonAttributes.noteType, + 'data-line-code': buttonAttributes.lineCode, + 'data-position': buttonAttributes.position, + 'data-discussion-id': buttonAttributes.discussionID, + 'data-line-type': buttonAttributes.lineType + }); + }; + + FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { + return hoveredElement.closest(TEXT_FILE_SELECTOR); + }; + + FilesCommentButton.prototype.getLineContent = function(hoveredElement) { + if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + return hoveredElement; + } + if (!this.isParallelView) { + return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); + } else { + return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + } + }; + + FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { + if (!this.isParallelView) { + if (hoveredElement.hasClass(OLD_LINE_CLASS)) { return hoveredElement; } - if (this.VIEW_TYPE === 'inline') { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); - } - }; - - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (this.VIEW_TYPE === 'inline') { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + return hoveredElement.parent().find("." + OLD_LINE_CLASS); + } else { + if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { + return hoveredElement; } - }; + return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + } + }; - FilesCommentButton.prototype.isMovingToSameType = function(e) { - var newButtonParent; - newButtonParent = this.getButtonParent($(e.toElement)); - if (!newButtonParent) { - return false; - } - return newButtonParent.is(this.getButtonParent($(e.currentTarget))); - }; + FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); + }; - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; - }; + FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { + return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; + }; - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; - }; + return FilesCommentButton; +})(); - return FilesCommentButton; - })(); +$.fn.filesCommentButton = function() { + $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); - $.fn.filesCommentButton = function() { - if (!(this && (this.parent().data('can-create-note') != null))) { - return; + if (!(this && (this.parent().data('can-create-note') != null))) { + return; + } + return this.each(function() { + if (!$.data(this, 'filesCommentButton')) { + return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); - } - }); - }; -}).call(window); + }); +}; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js new file mode 100644 index 00000000000..aaaeb9bddb1 --- /dev/null +++ b/app/assets/javascripts/filterable_list.js @@ -0,0 +1,46 @@ +/** + * Makes search request for content when user types a value in the search input. + * Updates the html content of the page with the received one. + */ + +export default class FilterableList { + constructor(form, filter, holder) { + this.filterForm = form; + this.listFilterElement = filter; + this.listHolderElement = holder; + } + + initSearch() { + this.debounceFilter = _.debounce(this.filterResults.bind(this), 500); + + this.listFilterElement.removeEventListener('input', this.debounceFilter); + this.listFilterElement.addEventListener('input', this.debounceFilter); + } + + filterResults() { + const form = this.filterForm; + const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; + + $(this.listHolderElement).fadeTo(250, 0.5); + + return $.ajax({ + url: form.getAttribute('action'), + data: $(form).serialize(), + type: 'GET', + dataType: 'json', + context: this, + complete() { + $(this.listHolderElement).fadeTo(250, 1); + }, + success(data) { + this.listHolderElement.innerHTML = data.html; + + // Change url so if user reload a page - search results are saved + return window.history.replaceState({ + page: filterUrl, + + }, document.title, filterUrl); + }, + }); + } +} diff --git a/app/assets/javascripts/filtered_search/container.js b/app/assets/javascripts/filtered_search/container.js new file mode 100644 index 00000000000..2243c4dd2c5 --- /dev/null +++ b/app/assets/javascripts/filtered_search/container.js @@ -0,0 +1,14 @@ +/* eslint-disable class-methods-use-this */ +let container = document; + +class FilteredSearchContainerClass { + set container(containerParam) { + container = containerParam; + } + + get container() { + return container; + } +} + +export default new FilteredSearchContainerClass(); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js new file mode 100644 index 00000000000..98dcb697af9 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -0,0 +1,83 @@ +require('./filtered_search_dropdown'); + +/* global droplabFilter */ + +(() => { + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + }, + }; + } + + itemClicked(e) { + const { selected } = e.detail; + + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + // Get previous input values in the input field and convert them into visual tokens + const previousInputValues = this.input.value.split(' '); + const searchTerms = []; + + previousInputValues.forEach((value, index) => { + searchTerms.push(value); + + if (index === previousInputValues.length - 1 + && token.indexOf(value.toLowerCase()) !== -1) { + searchTerms.pop(); + } + }); + + if (searchTerms.length > 0) { + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); + } + + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + } + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + } + + renderContent() { + const dropdownData = []; + + [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { + const { icon, hint, tag, type } = dropdownMenu.dataset; + if (icon && hint && tag) { + dropdownData.push( + Object.assign({ + icon: `fa-${icon}`, + hint, + tag: `<${tag}>`, + }, type && { type }), + ); + } + }); + + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 deleted file mode 100644 index 9e92d544bef..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -require('./filtered_search_dropdown'); - -/* global droplabFilter */ - -(() => { - class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - droplabFilter: { - template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, input), - }, - }; - } - - itemClicked(e) { - const { selected } = e.detail; - - if (selected.tagName === 'LI') { - if (selected.hasAttribute('data-value')) { - this.dismissDropdown(); - } else if (selected.getAttribute('data-action') === 'submit') { - this.dismissDropdown(); - this.dispatchFormSubmitEvent(); - } else { - const token = selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - - if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); - } - this.dismissDropdown(); - this.dispatchInputEvent(); - } - } - } - - renderContent() { - const dropdownData = []; - - [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => { - const { icon, hint, tag } = dropdownMenu.dataset; - if (icon && hint && tag) { - dropdownData.push({ - icon: `fa-${icon}`, - hint, - tag: `<${tag}>`, - }); - } - }); - - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - this.droplab.setData(this.hookId, dropdownData); - } - - init() { - this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); - } - } - - window.gl = window.gl || {}; - gl.DropdownHint = DropdownHint; -})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js index b3dc3e502c5..b3dc3e502c5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js new file mode 100644 index 00000000000..04e2afad02f --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -0,0 +1,65 @@ +require('./filtered_search_dropdown'); + +/* global droplabAjaxFilter */ + +(() => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabAjaxFilter: { + endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + + getSearchInput() { + const query = gl.DropdownUtils.getSearchInput(this.input); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + + let value = lastToken || ''; + + if (value[0] === '@') { + value = value.slice(1); + } + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); + } + + return value; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 deleted file mode 100644 index 7e9c6f74aa5..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ /dev/null @@ -1,60 +0,0 @@ -require('./filtered_search_dropdown'); - -/* global droplabAjaxFilter */ - -(() => { - class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - super(droplab, dropdown, input, filter); - this.config = { - droplabAjaxFilter: { - endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput.bind(this), - loadingTemplate: this.loadingTemplate, - }, - }; - } - - itemClicked(e) { - super.itemClicked(e, - selected => selected.querySelector('.dropdown-light-content').innerText.trim()); - } - - renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); - super.renderContent(forceShowList); - } - - getProjectId() { - return this.input.getAttribute('data-project-id'); - } - - getSearchInput() { - const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - let value = lastToken.value || ''; - - // Removes the first character if it is a quotation so that we can search - // with multiple words - if (value[0] === '"' || value[0] === '\'') { - value = value.slice(1); - } - - return value; - } - - init() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); - } - } - - window.gl = window.gl || {}; - gl.DropdownUser = DropdownUser; -})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js new file mode 100644 index 00000000000..432b0c0dfd2 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -0,0 +1,181 @@ +import FilteredSearchContainer from './container'; + +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, input, item) { + const updatedItem = item; + const searchInput = gl.DropdownUtils.getSearchInput(input); + + const title = updatedItem.title.toLowerCase(); + let value = searchInput.toLowerCase(); + let symbol = ''; + + // Remove the symbol for filter + if (value[0] === filterSymbol) { + symbol = value[0]; + value = value.slice(1); + } + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + + return updatedItem; + } + + static filterHint(input, item) { + const updatedItem = item; + const searchInput = gl.DropdownUtils.getSearchQuery(input); + const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); + const lastKey = lastToken.key || lastToken || ''; + const allowMultiple = item.type === 'array'; + const itemInExistingTokens = tokens.some(t => t.key === item.hint); + + if (!allowMultiple && itemInExistingTokens) { + updatedItem.droplab_hidden = true; + } else if (!lastKey || searchInput.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastKey) { + const split = lastKey.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; + } + + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + + // Determines the full search query (visual tokens + input) + static getSearchQuery(untilInput = false) { + const container = FilteredSearchContainer.container; + const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); + const values = []; + + if (untilInput) { + const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); + // Add one to include input-token to the tokens array + tokens.splice(inputIndex + 1); + } + + tokens.forEach((token) => { + if (token.classList.contains('js-visual-token')) { + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; + let valueText = ''; + + if (value && value.innerText) { + valueText = value.innerText; + } + + if (token.className.indexOf('filtered-search-token') !== -1) { + values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); + } else { + values.push(name.innerText); + } + } else if (token.classList.contains('input-token')) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + const inputValue = input && input.value; + + if (isLastVisualTokenValid) { + values.push(inputValue); + } else { + const previous = values.pop(); + values.push(`${previous}${inputValue}`); + } + } + }); + + return values.join(' '); + } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // (will continue to match entire string until an end quote is found if any) + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 deleted file mode 100644 index de3fa116717..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ /dev/null @@ -1,126 +0,0 @@ -(() => { - class DropdownUtils { - static getEscapedText(text) { - let escapedText = text; - const hasSpace = text.indexOf(' ') !== -1; - const hasDoubleQuote = text.indexOf('"') !== -1; - - // Encapsulate value with quotes if it has spaces - // Known side effect: values's with both single and double quotes - // won't escape properly - if (hasSpace) { - if (hasDoubleQuote) { - escapedText = `'${text}'`; - } else { - // Encapsulate singleQuotes or if it hasSpace - escapedText = `"${text}"`; - } - } - - return escapedText; - } - - static filterWithSymbol(filterSymbol, input, item) { - const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); - - if (lastToken !== searchToken) { - const title = updatedItem.title.toLowerCase(); - let value = lastToken.value.toLowerCase(); - - // Removes the first character if it is a quotation so that we can search - // with multiple words - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } - - // Eg. filterSymbol = ~ for labels - const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; - const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; - - updatedItem.droplab_hidden = !match && !matchWithoutSymbol; - } else { - updatedItem.droplab_hidden = false; - } - - return updatedItem; - } - - static filterHint(input, item) { - const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - lastToken = lastToken.key || lastToken || ''; - - if (!lastToken || query.split('').last() === ' ') { - updatedItem.droplab_hidden = false; - } else if (lastToken) { - const split = lastToken.split(':'); - const tokenName = split[0].split(' ').last(); - - const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; - updatedItem.droplab_hidden = tokenName ? match : false; - } - - return updatedItem; - } - - static setDataValueIfSelected(filter, selected) { - const dataValue = selected.getAttribute('data-value'); - - if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); - } - - // Return boolean based on whether it was set - return dataValue !== null; - } - - static getSearchInput(filteredSearchInput) { - const inputValue = filteredSearchInput.value; - const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); - - return inputValue.slice(0, right); - } - - static getInputSelectionPosition(input) { - const selectionStart = input.selectionStart; - let inputValue = input.value; - // Replace all spaces inside quote marks with underscores - // (will continue to match entire string until an end quote is found if any) - // This helps with matching the beginning & end of a token:key - inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); - - // Get the right position for the word selected - // Regex matches first space - let right = inputValue.slice(selectionStart).search(/\s/); - - if (right >= 0) { - right += selectionStart; - } else if (right < 0) { - right = inputValue.length; - } - - // Get the left position for the word selected - // Regex matches last non-whitespace character - let left = inputValue.slice(0, right).search(/\S+$/); - - if (selectionStart === 0) { - left = 0; - } else if (selectionStart === inputValue.length && left < 0) { - left = inputValue.length; - } else if (left < 0) { - left = selectionStart; - } - - return { - left, - right, - }; - } - } - - window.gl = window.gl || {}; - gl.DropdownUtils = DropdownUtils; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 392f1835966..856eb6590ee 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,3 +1,10 @@ -function requireAll(context) { return context.keys().map(context); } - -requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/)); +require('./dropdown_hint'); +require('./dropdown_non_user'); +require('./dropdown_user'); +require('./dropdown_utils'); +require('./filtered_search_dropdown_manager'); +require('./filtered_search_dropdown'); +require('./filtered_search_manager'); +require('./filtered_search_token_keys'); +require('./filtered_search_tokenizer'); +require('./filtered_search_visual_tokens'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js new file mode 100644 index 00000000000..e7bf530d343 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -0,0 +1,124 @@ +(() => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input && input.getAttribute('data-id'); + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `<div class="filter-dropdown-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div>`; + this.bindEvents(); + } + + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } + + itemClicked(e, getValueFunction) { + const { selected } = e.detail; + + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); + } + + this.resetFilters(); + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + if (window.innerWidth > 480) { + this.dropdown.style.left = `${offset}px`; + } else { + this.dropdown.style.left = '0px'; + } + } + + renderContent(forceShowList = false) { + const currentHook = this.getCurrentHook(); + if (forceShowList && currentHook && currentHook.list.hidden) { + currentHook.list.show(); + } + } + + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); + + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; + + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); + } + } + + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } + + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } + + hideDropdown() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); + } + } + + resetFilters() { + const hook = this.getCurrentHook(); + + if (hook) { + const data = hook.list.data || []; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); + } + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 deleted file mode 100644 index fbc72a3001a..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ /dev/null @@ -1,119 +0,0 @@ -(() => { - const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; - - class FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { - this.droplab = droplab; - this.hookId = input && input.getAttribute('data-id'); - this.input = input; - this.filter = filter; - this.dropdown = dropdown; - this.loadingTemplate = `<div class="filter-dropdown-loading"> - <i class="fa fa-spinner fa-spin"></i> - </div>`; - this.bindEvents(); - } - - bindEvents() { - this.itemClickedWrapper = this.itemClicked.bind(this); - this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); - } - - unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); - } - - getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; - } - - itemClicked(e, getValueFunction) { - const { selected } = e.detail; - - if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); - - if (!dataValueSet) { - const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); - } - - this.dismissDropdown(); - this.dispatchInputEvent(); - } - } - - setAsDropdown() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); - } - - setOffset(offset = 0) { - this.dropdown.style.left = `${offset}px`; - } - - renderContent(forceShowList = false) { - const currentHook = this.getCurrentHook(); - if (forceShowList && currentHook && currentHook.list.hidden) { - currentHook.list.show(); - } - } - - render(forceRenderContent = false, forceShowList = false) { - this.setAsDropdown(); - - const currentHook = this.getCurrentHook(); - const firstTimeInitialized = currentHook === null; - - if (firstTimeInitialized || forceRenderContent) { - this.renderContent(forceShowList); - } else if (currentHook.list.list.id !== this.dropdown.id) { - this.renderContent(forceShowList); - } - } - - dismissDropdown() { - // Focusing on the input will dismiss dropdown - // (default droplab functionality) - this.input.focus(); - } - - dispatchInputEvent() { - // Propogate input change to FilteredSearchDropdownManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new CustomEvent('input', { - bubbles: true, - cancelable: true, - })); - } - - dispatchFormSubmitEvent() { - // dispatchEvent() is necessary as form.submit() does not - // trigger event handlers - this.input.form.dispatchEvent(new Event('submit')); - } - - hideDropdown() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } - } - - resetFilters() { - const hook = this.getCurrentHook(); - - if (hook) { - const data = hook.list.data; - const results = data.map((o) => { - const updated = o; - updated.droplab_hidden = false; - return updated; - }); - hook.list.render(results); - } - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchDropdown = FilteredSearchDropdown; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js new file mode 100644 index 00000000000..5fbe0450bb8 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -0,0 +1,191 @@ +/* global DropLab */ +import FilteredSearchContainer from './container'; + +(() => { + class FilteredSearchDropdownManager { + constructor(baseEndpoint = '', page) { + this.container = FilteredSearchContainer.container; + this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + this.filteredSearchInput = this.container.querySelector('.filtered-search'); + this.page = page; + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: this.container.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: this.container.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], + element: this.container.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], + element: this.container.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: this.container.querySelector('#js-dropdown-hint'), + }, + }; + } + + static addWordToInput(tokenName, tokenValue = '', clicked = false) { + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + input.value = ''; + + if (clicked) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + } + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + // Always align dropdown with the input field + let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; + + const maxInputWidth = 240; + const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; + + // Make sure offset never exceeds the input container + const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; + + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + + // Passing glArguments to `new gl[glClass](<arguments>)` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + } + + if (firstLoad) { + mappingKey.reference.init(); + } + + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } + + setDropdown() { + const query = gl.DropdownUtils.getSearchQuery(true); + const { lastToken, searchToken } = this.tokenizer.processTokens(query); + + if (this.currentDropdown) { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + if (!this.currentDropdown) { + return; + } + + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 deleted file mode 100644 index cecd3518ce3..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ /dev/null @@ -1,210 +0,0 @@ -/* global DropLab */ - -(() => { - class FilteredSearchDropdownManager { - constructor(baseEndpoint = '', page) { - this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); - this.tokenizer = gl.FilteredSearchTokenizer; - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; - this.filteredSearchInput = document.querySelector('.filtered-search'); - this.page = page; - - this.setupMapping(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - - cleanup() { - if (this.droplab) { - this.droplab.destroy(); - this.droplab = null; - } - - this.setupMapping(); - - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - setupMapping() { - this.mapping = { - author: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], - element: document.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], - element: document.querySelector('#js-dropdown-label'), - }, - hint: { - reference: null, - gl: 'DropdownHint', - element: document.querySelector('#js-dropdown-hint'), - }, - }; - } - - static addWordToInput(tokenName, tokenValue = '') { - const input = document.querySelector('.filtered-search'); - const inputValue = input.value; - const word = `${tokenName}:${tokenValue}`; - - // Get the string to replace - let newCaretPosition = input.selectionStart; - const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); - - input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; - - // If we have added a tokenValue at the end of the input, - // add a space and set selection to the end - if (right >= inputValue.length && tokenValue !== '') { - input.value += ' '; - newCaretPosition = input.value.length; - } - - gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); - } - - static updateInputCaretPosition(selectionStart, input) { - // Reset the position - // Sometimes can end up at end of input - input.setSelectionRange(selectionStart, selectionStart); - - const { right } = gl.DropdownUtils.getInputSelectionPosition(input); - - input.setSelectionRange(right, right); - } - - updateCurrentDropdownOffset() { - this.updateDropdownOffset(this.currentDropdown); - } - - updateDropdownOffset(key) { - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - - const input = this.filteredSearchInput; - const inputText = input.value.slice(0, input.selectionStart); - const filterIconPadding = 27; - let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; - - const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : - this.mapping[key].element.clientWidth; - const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; - - if (offsetMaxWidth < offset) { - offset = offsetMaxWidth; - } - - this.mapping[key].reference.setOffset(offset); - } - - load(key, firstLoad = false) { - const mappingKey = this.mapping[key]; - const glClass = mappingKey.gl; - const element = mappingKey.element; - let forceShowList = false; - - if (!mappingKey.reference) { - const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; - const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); - - // Passing glArguments to `new gl[glClass](<arguments>)` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); - } - - if (firstLoad) { - mappingKey.reference.init(); - } - - if (this.currentDropdown === 'hint') { - // Force the dropdown to show if it was clicked from the hint dropdown - forceShowList = true; - } - - this.updateDropdownOffset(key); - mappingKey.reference.render(firstLoad, forceShowList); - - this.currentDropdown = key; - } - - loadDropdown(dropdownName = '') { - let firstLoad = false; - - if (!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } - - const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && this.mapping[match.key]; - const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - - if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - const key = match && match.key ? match.key : 'hint'; - this.load(key, firstLoad); - } - } - - setDropdown() { - const { lastToken, searchToken } = this.tokenizer - .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); - - if (this.currentDropdown) { - this.updateCurrentDropdownOffset(); - } - - if (lastToken === searchToken && lastToken !== null) { - // Token is not fully initialized yet because it has no value - // Eg. token = 'label:' - - const split = lastToken.split(':'); - const dropdownName = split[0].split(' ').last(); - this.loadDropdown(split.length > 1 ? dropdownName : ''); - } else if (lastToken) { - // Token has been initialized into an object because it has a value - this.loadDropdown(lastToken.key); - } else { - this.loadDropdown('hint'); - } - } - - resetDropdowns() { - // Force current dropdown to hide - this.mapping[this.currentDropdown].reference.hideDropdown(); - - // Re-Load dropdown - this.setDropdown(); - - // Reset filters for current dropdown - this.mapping[this.currentDropdown].reference.resetFilters(); - - // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); - } - - destroyDroplab() { - this.droplab.destroy(); - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js new file mode 100644 index 00000000000..c6bb7fda8f2 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -0,0 +1,423 @@ +import FilteredSearchContainer from './container'; + +(() => { + class FilteredSearchManager { + constructor(page) { + this.container = FilteredSearchContainer.container; + this.filteredSearchInput = this.container.querySelector('.filtered-search'); + this.clearSearchButton = this.container.querySelector('.clear-search'); + this.tokensContainer = this.container.querySelector('.tokens-container'); + this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; + + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); + + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this); + this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); + this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); + this.editTokenWrapper = this.editToken.bind(this); + this.tokenChange = this.tokenChange.bind(this); + this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); + this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); + + this.filteredSearchInputForm = this.filteredSearchInput.form; + this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.addEventListener('click', this.tokenChange); + this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); + this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.addEventListener('click', this.unselectEditTokensWrapper); + document.addEventListener('click', this.removeInputContainerFocusWrapper); + document.addEventListener('keydown', this.removeSelectedTokenWrapper); + } + + unbindEvents() { + this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.removeEventListener('click', this.tokenChange); + this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); + this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.removeEventListener('click', this.unselectEditTokensWrapper); + document.removeEventListener('click', this.removeInputContainerFocusWrapper); + document.removeEventListener('keydown', this.removeSelectedTokenWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (this.filteredSearchInput.value === '' && lastVisualToken) { + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + } + + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } + + checkForEnter(e) { + if (e.keyCode === 38 || e.keyCode === 40) { + const selectionStart = this.filteredSearchInput.selectionStart; + + e.preventDefault(); + this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); + } + + if (e.keyCode === 13) { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const dropdownEl = dropdown.element; + const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); + + e.preventDefault(); + + if (!activeElements.length) { + if (this.isHandledAsync) { + e.stopImmediatePropagation(); + + this.filteredSearchInput.blur(); + this.dropdownManager.resetDropdowns(); + } else { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + } + + this.search(); + } + } + } + + addInputContainerFocus() { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + + if (inputContainer) { + inputContainer.classList.add('focus'); + } + } + + removeInputContainerFocus(e) { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); + const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; + const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; + + if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && + !isElementInStaticFilterDropdown && inputContainer) { + inputContainer.classList.remove('focus'); + } + } + + static selectToken(e) { + const button = e.target.closest('.selectable'); + + if (button) { + e.preventDefault(); + e.stopPropagation(); + gl.FilteredSearchVisualTokens.selectToken(button); + } + } + + unselectEditTokens(e) { + const inputContainer = this.container.querySelector('.filtered-search-input-container'); + const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); + const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; + const isElementTokensContainer = e.target.classList.contains('tokens-container'); + + if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + this.dropdownManager.resetDropdowns(); + } + } + + editToken(e) { + const token = e.target.closest('.js-visual-token'); + + if (token) { + gl.FilteredSearchVisualTokens.editToken(token); + this.tokenChange(); + } + } + + toggleClearSearchButton() { + const query = gl.DropdownUtils.getSearchQuery(); + const hidden = 'hidden'; + const hasHidden = this.clearSearchButton.classList.contains(hidden); + + if (query.length === 0 && !hasHidden) { + this.clearSearchButton.classList.add(hidden); + } else if (query.length && hasHidden) { + this.clearSearchButton.classList.remove(hidden); + } + } + + handleInputPlaceholder() { + const query = gl.DropdownUtils.getSearchQuery(); + const placeholder = 'Search or filter results...'; + const currentPlaceholder = this.filteredSearchInput.placeholder; + + if (query.length === 0 && currentPlaceholder !== placeholder) { + this.filteredSearchInput.placeholder = placeholder; + } else if (query.length > 0 && currentPlaceholder !== '') { + this.filteredSearchInput.placeholder = ''; + } + } + + removeSelectedToken(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + gl.FilteredSearchVisualTokens.removeSelectedToken(); + this.handleInputPlaceholder(); + this.toggleClearSearchButton(); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + + const removeElements = []; + + [].forEach.call(this.tokensContainer.children, (t) => { + if (t.classList.contains('js-visual-token')) { + removeElements.push(t); + } + }); + + removeElements.forEach((el) => { + el.parentElement.removeChild(el); + }); + + this.clearSearchButton.classList.add('hidden'); + this.handleInputPlaceholder(); + + this.dropdownManager.resetDropdowns(); + + if (this.isHandledAsync) { + this.search(); + } + } + + handleInputVisualToken() { + const input = this.filteredSearchInput; + const { tokens, searchToken } + = gl.FilteredSearchTokenizer.processTokens(input.value); + const { isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (isLastVisualTokenValid) { + tokens.forEach((t) => { + input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); + gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + }); + + const fragments = searchToken.split(':'); + if (fragments.length > 1) { + const inputValues = fragments[0].split(' '); + const tokenKey = inputValues.last(); + + if (inputValues.length > 1) { + inputValues.pop(); + const searchTerms = inputValues.join(' '); + + input.value = input.value.replace(searchTerms, ''); + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); + } + + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + input.value = input.value.replace(`${tokenKey}:`, ''); + } + } else { + // Keep listening to token until we determine that the user is done typing the token value + const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; + + if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { + gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + + // Trim the last space as seen in the if statement above + input.value = input.value.replace(searchToken, '').trim(); + } + } + } + + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); + let hasFilteredSearch = false; + + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in this.filteredSearchTokenKeys + const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); + + if (condition) { + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'search') { + hasFilteredSearch = true; + this.filteredSearchInput.value = sanitizedValue; + } + } + }); + + if (hasFilteredSearch) { + this.clearSearchButton.classList.remove('hidden'); + this.handleInputPlaceholder(); + } + } + + search() { + const paths = []; + const { tokens, searchToken } + = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery()); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); + + tokens.forEach((token) => { + const condition = this.filteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; + + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + } + + paths.push(tokenPath); + }); + + if (searchToken) { + const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + paths.push(`search=${sanitized}`); + } + + const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + + if (this.updateObject) { + this.updateObject(parameterizedUrl); + } else { + gl.utils.visitUrl(parameterizedUrl); + } + } + + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + + if (dropdown) { + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 deleted file mode 100644 index bbafead0305..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ /dev/null @@ -1,231 +0,0 @@ -(() => { - class FilteredSearchManager { - constructor(page) { - this.filteredSearchInput = document.querySelector('.filtered-search'); - this.clearSearchButton = document.querySelector('.clear-search'); - this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; - - if (this.filteredSearchInput) { - this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); - - this.bindEvents(); - this.loadSearchParamsFromURL(); - this.dropdownManager.setDropdown(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - } - - cleanup() { - this.unbindEvents(); - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - bindEvents() { - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); - this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); - this.checkForEnterWrapper = this.checkForEnter.bind(this); - this.clearSearchWrapper = this.clearSearch.bind(this); - this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); - this.tokenChange = this.tokenChange.bind(this); - - this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); - this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); - this.filteredSearchInput.addEventListener('click', this.tokenChange); - this.filteredSearchInput.addEventListener('keyup', this.tokenChange); - this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); - } - - unbindEvents() { - this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); - this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); - this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); - this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); - this.filteredSearchInput.removeEventListener('click', this.tokenChange); - this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); - this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); - } - - checkForBackspace(e) { - // 8 = Backspace Key - // 46 = Delete Key - if (e.keyCode === 8 || e.keyCode === 46) { - // Reposition dropdown so that it is aligned with cursor - this.dropdownManager.updateCurrentDropdownOffset(); - } - } - - checkForEnter(e) { - if (e.keyCode === 38 || e.keyCode === 40) { - const selectionStart = this.filteredSearchInput.selectionStart; - - e.preventDefault(); - this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); - } - - if (e.keyCode === 13) { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const dropdownEl = dropdown.element; - const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); - - e.preventDefault(); - - if (!activeElements.length) { - // Prevent droplab from opening dropdown - this.dropdownManager.destroyDroplab(); - - this.search(); - } - } - } - - toggleClearSearchButton(e) { - if (e.target.value) { - this.clearSearchButton.classList.remove('hidden'); - } else { - this.clearSearchButton.classList.add('hidden'); - } - } - - clearSearch(e) { - e.preventDefault(); - - this.filteredSearchInput.value = ''; - this.clearSearchButton.classList.add('hidden'); - - this.dropdownManager.resetDropdowns(); - } - - handleFormSubmit(e) { - e.preventDefault(); - this.search(); - } - - loadSearchParamsFromURL() { - const params = gl.utils.getUrlParamsArray(); - const usernameParams = this.getUsernameParams(); - const inputValues = []; - - params.forEach((p) => { - const split = p.split('='); - const keyParam = decodeURIComponent(split[0]); - const value = split[1]; - - // Check if it matches edge conditions listed in this.filteredSearchTokenKeys - const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); - - if (condition) { - inputValues.push(`${condition.tokenKey}:${condition.value}`); - } else { - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam); - - if (match) { - const indexOf = keyParam.indexOf('_'); - const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; - const symbol = match.symbol; - let quotationsToUse = ''; - - if (sanitizedValue.indexOf(' ') !== -1) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; - } - - inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); - } else if (!match && keyParam === 'assignee_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - inputValues.push(`assignee:@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'author_id') { - const id = parseInt(value, 10); - if (usernameParams[id]) { - inputValues.push(`author:@${usernameParams[id]}`); - } - } else if (!match && keyParam === 'search') { - inputValues.push(sanitizedValue); - } - } - }); - - // Trim the last space value - this.filteredSearchInput.value = inputValues.join(' '); - - if (inputValues.length > 0) { - this.clearSearchButton.classList.remove('hidden'); - } - } - - search() { - const paths = []; - const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - const currentState = gl.utils.getParameterByName('state') || 'opened'; - paths.push(`state=${currentState}`); - - tokens.forEach((token) => { - const condition = this.filteredSearchTokenKeys - .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; - const keyParam = param ? `${token.key}_${param}` : token.key; - let tokenPath = ''; - - if (condition) { - tokenPath = condition.url; - } else { - let tokenValue = token.value; - - if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || - (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { - tokenValue = tokenValue.slice(1, tokenValue.length - 1); - } - - tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; - } - - paths.push(tokenPath); - }); - - if (searchToken) { - const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); - paths.push(`search=${sanitized}`); - } - - const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; - - gl.utils.visitUrl(parameterizedUrl); - } - - getUsernameParams() { - const usernamesById = {}; - try { - const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach((user) => { - usernamesById[user.id] = user.username; - }); - } catch (e) { - // do nothing - } - return usernamesById; - } - - tokenChange() { - const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const currentDropdownRef = dropdown.reference; - - this.setDropdownWrapper(); - currentDropdownRef.dispatchInputEvent(); - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchManager = FilteredSearchManager; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js new file mode 100644 index 00000000000..6d5df86f2a5 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -0,0 +1,100 @@ +(() => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', + }]; + + const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'milestone_title=%23started', + tokenKey: 'milestone', + value: 'started', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + + class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } + + static getAlternatives() { + return alternativeTokenKeys; + } + + static getConditions() { + return conditions; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeysWithAlternative.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 deleted file mode 100644 index e6b53cd4b55..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ /dev/null @@ -1,96 +0,0 @@ -(() => { - const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - }]; - - const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', - }]; - - const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); - - const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', - }, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', - }, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', - }, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', - }]; - - class FilteredSearchTokenKeys { - static get() { - return tokenKeys; - } - - static getAlternatives() { - return alternativeTokenKeys; - } - - static getConditions() { - return conditions; - } - - static searchByKey(key) { - return tokenKeys.find(tokenKey => tokenKey.key === key) || null; - } - - static searchBySymbol(symbol) { - return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; - } - - static searchByKeyParam(keyParam) { - return tokenKeysWithAlternative.find((tokenKey) => { - let tokenKeyParam = tokenKey.key; - - if (tokenKey.param) { - tokenKeyParam += `_${tokenKey.param}`; - } - - return keyParam === tokenKeyParam; - }) || null; - } - - static searchByConditionUrl(url) { - return conditions.find(condition => condition.url === url) || null; - } - - static searchByConditionKeyValue(key, value) { - return conditions - .find(condition => condition.tokenKey === key && condition.value === value) || null; - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index 9bf1b1ced88..9bf1b1ced88 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js new file mode 100644 index 00000000000..a5657fc8720 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -0,0 +1,202 @@ +import FilteredSearchContainer from './container'; + +class FilteredSearchVisualTokens { + static getLastVisualTokenBeforeInput() { + const inputLi = FilteredSearchContainer.container.querySelector('.input-token'); + const lastVisualToken = inputLi && inputLi.previousElementSibling; + + return { + lastVisualToken, + isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null), + }; + } + + static unselectTokens() { + const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); + [].forEach.call(otherTokens, t => t.classList.remove('selected')); + } + + static selectToken(tokenButton) { + const selected = tokenButton.classList.contains('selected'); + FilteredSearchVisualTokens.unselectTokens(); + + if (!selected) { + tokenButton.classList.add('selected'); + } + } + + static removeSelectedToken() { + const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected'); + + if (selected) { + const li = selected.closest('.js-visual-token'); + li.parentElement.removeChild(li); + } + } + + static createVisualTokenElementHTML() { + return ` + <div class="selectable" role="button"> + <div class="name"></div> + <div class="value"></div> + </div> + `; + } + + static addVisualTokenElement(name, value, isSearchTerm) { + const li = document.createElement('li'); + li.classList.add('js-visual-token'); + li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); + + if (value) { + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); + li.querySelector('.value').innerText = value; + } else { + li.innerHTML = '<div class="name"></div>'; + } + li.querySelector('.name').innerText = name; + + const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + tokensContainer.insertBefore(li, input.parentElement); + } + + static addValueToPreviousVisualTokenElement(value) { + const { lastVisualToken, isLastVisualTokenValid } = + FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) { + const name = FilteredSearchVisualTokens.getLastTokenPartial(); + lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); + lastVisualToken.querySelector('.name').innerText = name; + lastVisualToken.querySelector('.value').innerText = value; + } + } + + static addFilterVisualToken(tokenName, tokenValue) { + const { lastVisualToken, isLastVisualTokenValid } + = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; + + if (isLastVisualTokenValid) { + addVisualTokenElement(tokenName, tokenValue, false); + } else { + const previousTokenName = lastVisualToken.querySelector('.name').innerText; + const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); + tokensContainer.removeChild(lastVisualToken); + + const value = tokenValue || tokenName; + addVisualTokenElement(previousTokenName, value, false); + } + } + + static addSearchVisualToken(searchTerm) { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) { + lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`; + } else { + FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); + } + } + + static getLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!lastVisualToken) return ''; + + const value = lastVisualToken.querySelector('.value'); + const name = lastVisualToken.querySelector('.name'); + + const valueText = value ? value.innerText : ''; + const nameText = name ? name.innerText : ''; + + return valueText || nameText; + } + + static removeLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (lastVisualToken) { + const value = lastVisualToken.querySelector('.value'); + + if (value) { + const button = lastVisualToken.querySelector('.selectable'); + button.removeChild(value); + lastVisualToken.innerHTML = button.innerHTML; + } else { + lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); + } + } + } + + static tokenizeInput() { + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (input.value) { + if (isLastVisualTokenValid) { + gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value); + } else { + FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value); + } + + input.value = ''; + } + } + + static editToken(token) { + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + + FilteredSearchVisualTokens.tokenizeInput(); + + // Replace token with input field + const tokenContainer = token.parentElement; + const inputLi = input.parentElement; + tokenContainer.replaceChild(inputLi, token); + + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + + if (token.classList.contains('filtered-search-token') && value) { + FilteredSearchVisualTokens.addFilterVisualToken(name.innerText); + input.value = value.innerText; + } else { + // token is a search term + input.value = name.innerText; + } + + // Opens dropdown + const inputEvent = new Event('input'); + input.dispatchEvent(inputEvent); + + // Adds cursor to input + input.focus(); + } + + static moveInputToTheRight() { + const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + const inputLi = input.parentElement; + const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); + + FilteredSearchVisualTokens.tokenizeInput(); + + if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!isLastVisualTokenValid) { + const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial); + } + + tokenContainer.removeChild(inputLi); + tokenContainer.appendChild(inputLi); + } + } +} + +window.gl = window.gl || {}; +gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 730104b89f9..eec30624ff2 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,42 +1,41 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ -(function() { - this.Flash = (function() { - var hideFlash; - hideFlash = function() { - return $(this).fadeOut(); - }; +window.Flash = (function() { + var hideFlash; - function Flash(message, type, parent) { - var flash, textDiv; - if (type == null) { - type = 'alert'; - } - if (parent == null) { - parent = null; - } - if (parent) { - this.flashContainer = parent.find('.flash-container'); - } else { - this.flashContainer = $('.flash-container-page'); - } - this.flashContainer.html(''); - flash = $('<div/>', { - "class": "flash-" + type - }); - flash.on('click', hideFlash); - textDiv = $('<div/>', { - "class": 'flash-text', - text: message - }); - textDiv.appendTo(flash); - if (this.flashContainer.parent().hasClass('content-wrapper')) { - textDiv.addClass('container-fluid container-limited'); - } - flash.appendTo(this.flashContainer); - this.flashContainer.show(); + hideFlash = function() { + return $(this).fadeOut(); + }; + + function Flash(message, type, parent) { + var flash, textDiv; + if (type == null) { + type = 'alert'; + } + if (parent == null) { + parent = null; + } + if (parent) { + this.flashContainer = parent.find('.flash-container'); + } else { + this.flashContainer = $('.flash-container-page'); + } + this.flashContainer.html(''); + flash = $('<div/>', { + "class": "flash-" + type + }); + flash.on('click', hideFlash); + textDiv = $('<div/>', { + "class": 'flash-text', + text: message + }); + textDiv.appendTo(flash); + if (this.flashContainer.parent().hasClass('content-wrapper')) { + textDiv.addClass('container-fluid container-limited'); } + flash.appendTo(this.flashContainer); + this.flashContainer.show(); + } - return Flash; - })(); -}).call(window); + return Flash; +})(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js new file mode 100644 index 00000000000..9ac4c49d697 --- /dev/null +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -0,0 +1,390 @@ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ + +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from '~/behaviors/gl_emoji'; + +// Creates the variables for setting up GFM auto-completion +window.gl = window.gl || {}; + +function sanitize(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); +} + +window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], + cachedData: {}, + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, + // Emoji + Emoji: { + templateFunction: function(name) { + return `<li> + ${name} ${glEmojiTag(name)} + </li> + `; + } + }, + // Team Members + Members: { + template: '<li>${avatarTag} ${username} <small>${title}</small></li>' + }, + Labels: { + template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' + }, + // Issues and MergeRequests + Issues: { + template: '<li><small>${id}</small> ${title}</li>' + }, + // Milestones + Milestones: { + template: '<li>${title}</li>' + }, + Loading: { + template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; + } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + }, + filter: function(query, data, searchKey) { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); + return data; + } else { + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + } + }, + beforeInsert: function(value) { + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } + return value; + }, + matcher: function (flag, subtext) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); + + match = regexp.exec(subtext); + + if (match) { + return match[1]; + } else { + return null; + } + } + }, + setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. + this.input = input || $('.js-gfm-input'); + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + // This triggers at.js again + // Needed for slash commands with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + }); + }, + setupAtWho: function($input) { + // Emoji + $input.atwho({ + at: ':', + displayTpl: function(value) { + return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; + }.bind(this), + insertTpl: ':${name}:', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter + } + }); + // Team Members + $input.atwho({ + at: '@', + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(members) { + return $.map(members, function(m) { + let title = ''; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } + + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; + const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; + + return { + username: m.username, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); + } + } + }); + $input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); + } + } + }); + $input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + insertTpl: '${atwho-at}${title}', + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + callbacks: { + matcher: this.DefaultOptions.matcher, + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + beforeSave: function(milestones) { + return $.map(milestones, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); + } + } + }); + $input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); + } + } + }); + $input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), + insertTpl: '${atwho-at}${title}', + callbacks: { + matcher: this.DefaultOptions.matcher, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, + beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; + return $.map(merges, function(m) { + return { + title: sanitize(m.title), + color: m.color, + search: "" + m.title + }; + }); + } + } + }); + // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms + $input.filter('[data-supports-slash-commands="true"]').atwho({ + at: '/', + alias: 'commands', + searchKey: 'search', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; + var tpl = '<li>/${name}'; + if (value.aliases.length > 0) { + tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; + } + if (value.params.length > 0) { + tpl += ' <small><%- params.join(" ") %></small>'; + } + if (value.description !== '') { + tpl += '<small class="description"><i><%- description %></i></small>'; + } + tpl += '</li>'; + return _.template(tpl)(value); + }.bind(this), + insertTpl: function(value) { + var tpl = "/${name} "; + var reference_prefix = null; + if (value.params.length > 0) { + reference_prefix = value.params[0][0]; + if (/^[@%~]/.test(reference_prefix)) { + tpl += '<%- reference_prefix %>'; + } + } + return _.template(tpl)({ reference_prefix: reference_prefix }); + }, + suffix: '', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; + return $.map(commands, function(c) { + var search = c.name; + if (c.aliases.length > 0) { + search = search + " " + c.aliases.join(" "); + } + return { + name: c.name, + aliases: c.aliases, + params: c.params, + description: c.description, + search: search + }; + }); + }, + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + var match = regexp.exec(subtext); + if (match) { + return match[1]; + } else { + return null; + } + } + } + }); + return; + }, + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else if (this.atTypeMap[at] === 'emojis') { + this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } + }, + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); + // This trigger at.js again + // otherwise we would be stuck with loading until the user types + return $input.trigger('keyup'); + }, + isLoading(data) { + var dataToInspect = data; + if (data && data.length > 0) { + dataToInspect = data[0]; + } + + var loadingState = this.defaultLoadingData[0]; + return dataToInspect && + (dataToInspect === loadingState || dataToInspect.name === loadingState); + } +}; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 deleted file mode 100644 index 60d6658dc16..00000000000 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ /dev/null @@ -1,383 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ - -// Creates the variables for setting up GFM auto-completion -(function() { - if (window.gl == null) { - window.gl = {}; - } - - function sanitize(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - } - - window.gl.GfmAutoComplete = { - dataSources: {}, - defaultLoadingData: ['loading'], - cachedData: {}, - isLoadingData: {}, - atTypeMap: { - ':': 'emojis', - '@': 'members', - '#': 'issues', - '!': 'mergeRequests', - '~': 'labels', - '%': 'milestones', - '/': 'commands' - }, - // Emoji - Emoji: { - template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' - }, - // Team Members - Members: { - template: '<li>${avatarTag} ${username} <small>${title}</small></li>' - }, - Labels: { - template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' - }, - // Issues and MergeRequests - Issues: { - template: '<li><small>${id}</small> ${title}</li>' - }, - // Milestones - Milestones: { - template: '<li>${title}</li>' - }, - Loading: { - template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>' - }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if (gl.GfmAutoComplete.isLoading(items)) { - this.setting.highlightFirst = false; - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.fetchData(this.$inputor, this.at); - return data; - } else { - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); - } - }, - beforeInsert: function(value) { - if (value && !this.setting.skipSpecialCharacterTest) { - var withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; - } - return value; - }, - matcher: function (flag, subtext) { - // The below is taken from At.js source - // Tweaked to commands to start without a space only if char before is a non-word character - // https://github.com/ichord/At.js - var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; - atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - subtext = subtext.split(/\s+/g).pop(); - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); - - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); - - match = regexp.exec(subtext); - - if (match) { - return match[1]; - } else { - return null; - } - } - }, - setup: function(input) { - // Add GFM auto-completion to all input fields, that accept GFM input. - this.input = input || $('.js-gfm-input'); - this.setupLifecycle(); - }, - setupLifecycle() { - this.input.each((i, input) => { - const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - }); - }, - setupAtWho: function($input) { - // Emoji - $input.atwho({ - at: ':', - displayTpl: function(value) { - return value.path != null ? this.Emoji.template : this.Loading.template; - }.bind(this), - insertTpl: ':${name}:', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter - } - }); - // Team Members - $input.atwho({ - at: '@', - displayTpl: function(value) { - return value.username != null ? this.Members.template : this.Loading.template; - }.bind(this), - insertTpl: '${atwho-at}${username}', - searchKey: 'search', - alwaysHighlightFirst: true, - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { - let title = ''; - if (m.username == null) { - return m; - } - title = m.name; - if (m.count) { - title += " (" + m.count + ")"; - } - - const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; - const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; - - return { - username: m.username, - avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, - title: sanitize(title), - search: sanitize(m.username + " " + m.name) - }; - }); - } - } - }); - $input.atwho({ - at: '#', - alias: 'issues', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { - if (i.title == null) { - return i; - } - return { - id: i.iid, - title: sanitize(i.title), - search: i.iid + " " + i.title - }; - }); - } - } - }); - $input.atwho({ - at: '%', - alias: 'milestones', - searchKey: 'search', - insertTpl: '${atwho-at}${title}', - displayTpl: function(value) { - return value.title != null ? this.Milestones.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - callbacks: { - matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - beforeSave: function(milestones) { - return $.map(milestones, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: "" + m.title - }; - }); - } - } - }); - $input.atwho({ - at: '!', - alias: 'mergerequests', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: m.iid + " " + m.title - }; - }); - } - } - }); - $input.atwho({ - at: '~', - alias: 'labels', - searchKey: 'search', - data: this.defaultLoadingData, - displayTpl: function(value) { - return this.isLoading(value) ? this.Loading.template : this.Labels.template; - }.bind(this), - insertTpl: '${atwho-at}${title}', - callbacks: { - matcher: this.DefaultOptions.matcher, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - sorter: this.DefaultOptions.sorter, - beforeSave: function(merges) { - if (gl.GfmAutoComplete.isLoading(merges)) return merges; - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } - }; - return $.map(merges, function(m) { - return { - title: sanitize(m.title), - color: m.color, - search: "" + m.title - }; - }); - } - } - }); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $input.filter('[data-supports-slash-commands="true"]').atwho({ - at: '/', - alias: 'commands', - searchKey: 'search', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - displayTpl: function(value) { - if (this.isLoading(value)) return this.Loading.template; - var tpl = '<li>/${name}'; - if (value.aliases.length > 0) { - tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; - } - if (value.params.length > 0) { - tpl += ' <small><%- params.join(" ") %></small>'; - } - if (value.description !== '') { - tpl += '<small class="description"><i><%- description %></i></small>'; - } - tpl += '</li>'; - return _.template(tpl)(value); - }.bind(this), - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; - if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; - } - } - return _.template(tpl)({ reference_prefix: reference_prefix }); - }, - suffix: '', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - if (gl.GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, function(c) { - var search = c.name; - if (c.aliases.length > 0) { - search = search + " " + c.aliases.join(" "); - } - return { - name: c.name, - aliases: c.aliases, - params: c.params, - description: c.description, - search: search - }; - }); - }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } - } - } - }); - return; - }, - fetchData: function($input, at) { - if (this.isLoadingData[at]) return; - this.isLoadingData[at] = true; - if (this.cachedData[at]) { - this.loadData($input, at, this.cachedData[at]); - } else { - $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { - this.loadData($input, at, data); - }).fail(() => { this.isLoadingData[at] = false; }); - } - }, - loadData: function($input, at, data) { - this.isLoadingData[at] = false; - this.cachedData[at] = data; - $input.atwho('load', at, data); - // This trigger at.js again - // otherwise we would be stuck with loading until the user types - return $input.trigger('keyup'); - }, - isLoading(data) { - var dataToInspect = data; - if (data && data.length > 0) { - dataToInspect = data[0]; - } - - var loadingState = this.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); - } - }; -}).call(window); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index a01662e2f9e..a03f1202a6d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,850 +1,848 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -(function() { - var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, - bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; - - GitLabDropdownFilter = (function() { - var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; - - BLUR_KEYCODES = [27, 40]; - - ARROW_KEY_CODES = [38, 40]; - - HAS_VALUE_CLASS = "has-value"; - - function GitLabDropdownFilter(input, options) { - var $clearButton, $inputContainer, ref, timeout; - this.input = input; - this.options = options; - this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; - $inputContainer = this.input.parent(); - $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', (function(_this) { - // Clear click - return function(e) { +var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, + bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; + +GitLabDropdownFilter = (function() { + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; + + BLUR_KEYCODES = [27, 40]; + + ARROW_KEY_CODES = [38, 40]; + + HAS_VALUE_CLASS = "has-value"; + + function GitLabDropdownFilter(input, options) { + var $clearButton, $inputContainer, ref, timeout; + this.input = input; + this.options = options; + this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; + $inputContainer = this.input.parent(); + $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + $clearButton.on('click', (function(_this) { + // Clear click + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.input.val('').trigger('input').focus(); + }; + })(this)); + // Key events + timeout = ""; + this.input + .on('keydown', function (e) { + var keyCode = e.which; + if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); - e.stopPropagation(); - return _this.input.val('').trigger('input').focus(); - }; - })(this)); - // Key events - timeout = ""; - this.input - .on('keydown', function (e) { - var keyCode = e.which; - if (keyCode === 13 && !options.elIsInput) { - e.preventDefault(); - } - }) - .on('input', function() { - if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.addClass(HAS_VALUE_CLASS); - } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.removeClass(HAS_VALUE_CLASS); - } - // Only filter asynchronously only if option remote is set - if (this.options.remote) { - clearTimeout(timeout); - return timeout = setTimeout(function() { - $inputContainer.parent().addClass('is-loading'); - - return this.options.query(this.input.val(), function(data) { - $inputContainer.parent().removeClass('is-loading'); - return this.options.callback(data); - }.bind(this)); - }.bind(this), 250); - } else { - return this.filter(this.input.val()); - } - }.bind(this)); - } + } + }) + .on('input', function() { + if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + // Only filter asynchronously only if option remote is set + if (this.options.remote) { + clearTimeout(timeout); + return timeout = setTimeout(function() { + $inputContainer.parent().addClass('is-loading'); + + return this.options.query(this.input.val(), function(data) { + $inputContainer.parent().removeClass('is-loading'); + return this.options.callback(data); + }.bind(this)); + }.bind(this), 250); + } else { + return this.filter(this.input.val()); + } + }.bind(this)); + } - GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) >= 0; - }; + GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) !== -1; + }; - GitLabDropdownFilter.prototype.filter = function(search_text) { - var data, elements, group, key, results, tmp; - if (this.options.onFilter) { - this.options.onFilter(search_text); - } - data = this.options.data(); - if ((data != null) && !this.options.filterByText) { - results = data; - if (search_text !== '') { - // When data is an array of objects therefore [object Array] e.g. - // [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ] - if (_.isArray(data)) { - results = fuzzaldrinPlus.filter(data, search_text, { - key: this.options.keys - }); - } else { - // If data is grouped therefore an [object Object]. e.g. - // { - // groupName1: [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ], - // groupName2: [ - // { prop: 'abc' }, - // { prop: 'def' } - // ] - // } - if (gl.utils.isObject(data)) { - results = {}; - for (key in data) { - group = data[key]; - tmp = fuzzaldrinPlus.filter(group, search_text, { - key: this.options.keys + GitLabDropdownFilter.prototype.filter = function(search_text) { + var data, elements, group, key, results, tmp; + if (this.options.onFilter) { + this.options.onFilter(search_text); + } + data = this.options.data(); + if ((data != null) && !this.options.filterByText) { + results = data; + if (search_text !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] + if (_.isArray(data)) { + results = fuzzaldrinPlus.filter(data, search_text, { + key: this.options.keys + }); + } else { + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } + if (gl.utils.isObject(data)) { + results = {}; + for (key in data) { + group = data[key]; + tmp = fuzzaldrinPlus.filter(group, search_text, { + key: this.options.keys + }); + if (tmp.length) { + results[key] = tmp.map(function(item) { + return item; }); - if (tmp.length) { - results[key] = tmp.map(function(item) { - return item; - }); - } } } } } - return this.options.callback(results); - } else { - elements = this.options.elements(); - if (search_text) { - return elements.each(function() { - var $el, matches; - $el = $(this); - matches = fuzzaldrinPlus.match($el.text().trim(), search_text); - if (!$el.is('.dropdown-header')) { - if (matches.length) { - return $el.show().removeClass('option-hidden'); - } else { - return $el.hide().addClass('option-hidden'); - } + } + return this.options.callback(results); + } else { + elements = this.options.elements(); + if (search_text) { + return elements.each(function() { + var $el, matches; + $el = $(this); + matches = fuzzaldrinPlus.match($el.text().trim(), search_text); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show().removeClass('option-hidden'); + } else { + return $el.hide().addClass('option-hidden'); } - }); - } else { - return elements.show().removeClass('option-hidden'); - } + } + }); + } else { + return elements.show().removeClass('option-hidden'); } - }; - - return GitLabDropdownFilter; - })(); + } + }; - GitLabDropdownRemote = (function() { - function GitLabDropdownRemote(dataEndpoint, options) { - this.dataEndpoint = dataEndpoint; - this.options = options; + return GitLabDropdownFilter; +})(); + +GitLabDropdownRemote = (function() { + function GitLabDropdownRemote(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + GitLabDropdownRemote.prototype.execute = function() { + if (typeof this.dataEndpoint === "string") { + return this.fetchData(); + } else if (typeof this.dataEndpoint === "function") { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint("", (function(_this) { + // Fetch the data by calling the data funcfion + return function(data) { + if (_this.options.success) { + _this.options.success(data); + } + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this)); } + }; - GitLabDropdownRemote.prototype.execute = function() { - if (typeof this.dataEndpoint === "string") { - return this.fetchData(); - } else if (typeof this.dataEndpoint === "function") { - if (this.options.beforeSend) { - this.options.beforeSend(); - } - return this.dataEndpoint("", (function(_this) { - // Fetch the data by calling the data funcfion - return function(data) { - if (_this.options.success) { - _this.options.success(data); - } - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this)); - } - }; + GitLabDropdownRemote.prototype.fetchData = function() { + return $.ajax({ + url: this.dataEndpoint, + dataType: this.options.dataType, + beforeSend: (function(_this) { + return function() { + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this), + success: (function(_this) { + return function(data) { + if (_this.options.success) { + return _this.options.success(data); + } + }; + })(this) + }); + // Fetch the data through ajax if the data is a string + }; - GitLabDropdownRemote.prototype.fetchData = function() { - return $.ajax({ - url: this.dataEndpoint, - dataType: this.options.dataType, - beforeSend: (function(_this) { - return function() { - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this), - success: (function(_this) { - return function(data) { - if (_this.options.success) { - return _this.options.success(data); - } - }; - })(this) - }); - // Fetch the data through ajax if the data is a string - }; + return GitLabDropdownRemote; +})(); - return GitLabDropdownRemote; - })(); +GitLabDropdown = (function() { + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; - GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; + LOADING_CLASS = "is-loading"; - LOADING_CLASS = "is-loading"; + PAGE_TWO_CLASS = "is-page-two"; - PAGE_TWO_CLASS = "is-page-two"; + ACTIVE_CLASS = "is-active"; - ACTIVE_CLASS = "is-active"; + INDETERMINATE_CLASS = "is-indeterminate"; - INDETERMINATE_CLASS = "is-indeterminate"; + currentIndex = -1; - currentIndex = -1; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - - SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; - - CURSOR_SELECT_SCROLL_PADDING = 5; - - FILTER_INPUT = '.dropdown-input .dropdown-input-field'; - - function GitLabDropdown(el1, options) { - var searchFields, selector, self; - this.el = el1; - this.options = options; - this.updateLabel = bind(this.updateLabel, this); - this.hidden = bind(this.hidden, this); - this.opened = bind(this.opened, this); - this.shouldPropagate = bind(this.shouldPropagate, this); - self = this; - selector = $(this.el).data("target"); - this.dropdown = selector != null ? $(selector) : $(this.el).parent(); - // Set Defaults - this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); - this.highlight = !!this.options.highlight; - this.filterInputBlur = this.options.filterInputBlur != null - ? this.options.filterInputBlur - : true; - // If no input is passed create a default one - self = this; - // If selector was passed - if (_.isString(this.filterInput)) { - this.filterInput = this.getElement(this.filterInput); - } - searchFields = this.options.search ? this.options.search.fields : []; - if (this.options.data) { - // If we provided data - // data could be an array of objects or a group of arrays - if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { - this.fullData = this.options.data; - currentIndex = -1; - this.parseData(this.options.data); - this.focusTextInput(); - } else { - this.remote = new GitLabDropdownRemote(this.options.data, { - dataType: this.options.dataType, - beforeSend: this.toggleLoading.bind(this), - success: (function(_this) { - return function(data) { - _this.fullData = data; - _this.parseData(_this.fullData); - _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { - return _this.filter.input.trigger('input'); - } - }; - // Remote data - })(this) - }); - } - } - // Init filterable - if (this.options.filterable) { - this.filter = new GitLabDropdownFilter(this.filterInput, { - elIsInput: $(this.el).is('input'), - filterInputBlur: this.filterInputBlur, - filterByText: this.options.filterByText, - onFilter: this.options.onFilter, - remote: this.options.filterRemote, - query: this.options.data, - keys: searchFields, - elements: (function(_this) { - return function() { - selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - return $(selector); - }; - })(this), - data: (function(_this) { - return function() { - return _this.fullData; - }; - })(this), - callback: (function(_this) { + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; + + CURSOR_SELECT_SCROLL_PADDING = 5; + + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; + + function GitLabDropdown(el1, options) { + var searchFields, selector, self; + this.el = el1; + this.options = options; + this.updateLabel = bind(this.updateLabel, this); + this.hidden = bind(this.hidden, this); + this.opened = bind(this.opened, this); + this.shouldPropagate = bind(this.shouldPropagate, this); + self = this; + selector = $(this.el).data("target"); + this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + // Set Defaults + this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); + this.highlight = !!this.options.highlight; + this.filterInputBlur = this.options.filterInputBlur != null + ? this.options.filterInputBlur + : true; + // If no input is passed create a default one + self = this; + // If selector was passed + if (_.isString(this.filterInput)) { + this.filterInput = this.getElement(this.filterInput); + } + searchFields = this.options.search ? this.options.search.fields : []; + if (this.options.data) { + // If we provided data + // data could be an array of objects or a group of arrays + if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { + this.fullData = this.options.data; + currentIndex = -1; + this.parseData(this.options.data); + this.focusTextInput(); + } else { + this.remote = new GitLabDropdownRemote(this.options.data, { + dataType: this.options.dataType, + beforeSend: this.toggleLoading.bind(this), + success: (function(_this) { return function(data) { - _this.parseData(data); - if (_this.filterInput.val() !== '') { - selector = SELECTABLE_CLASSES; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - if ($(_this.el).is('input')) { - currentIndex = -1; - } else { - $(selector, _this.dropdown).first().find('a').addClass('is-focused'); - currentIndex = 0; - } + _this.fullData = data; + _this.parseData(_this.fullData); + _this.focusTextInput(); + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { + return _this.filter.input.trigger('input'); } }; + // Remote data })(this) }); } - // Event listeners - this.dropdown.on("shown.bs.dropdown", this.opened); - this.dropdown.on("hidden.bs.dropdown", this.hidden); - $(this.el).on("update.label", this.updateLabel); - this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); - this.dropdown.on('keyup', (function(_this) { - return function(e) { - // Escape key - if (e.which === 27) { - return $('.dropdown-menu-close', _this.dropdown).trigger('click'); - } - }; - })(this)); - this.dropdown.on('blur', 'a', (function(_this) { - return function(e) { - var $dropdownMenu, $relatedTarget; - if (e.relatedTarget != null) { - $relatedTarget = $(e.relatedTarget); - $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); - if ($dropdownMenu.length === 0) { - return _this.dropdown.removeClass('open'); + } + // Init filterable + if (this.options.filterable) { + this.filter = new GitLabDropdownFilter(this.filterInput, { + elIsInput: $(this.el).is('input'), + filterInputBlur: this.filterInputBlur, + filterByText: this.options.filterByText, + onFilter: this.options.onFilter, + remote: this.options.filterRemote, + query: this.options.data, + keys: searchFields, + elements: (function(_this) { + return function() { + selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + return $(selector); + }; + })(this), + data: (function(_this) { + return function() { + return _this.fullData; + }; + })(this), + callback: (function(_this) { + return function(data) { + _this.parseData(data); + if (_this.filterInput.val() !== '') { + selector = SELECTABLE_CLASSES; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + if ($(_this.el).is('input')) { + currentIndex = -1; + } else { + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + currentIndex = 0; + } } + }; + })(this) + }); + } + // Event listeners + this.dropdown.on("shown.bs.dropdown", this.opened); + this.dropdown.on("hidden.bs.dropdown", this.hidden); + $(this.el).on("update.label", this.updateLabel); + this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); + this.dropdown.on('keyup', (function(_this) { + return function(e) { + // Escape key + if (e.which === 27) { + return $('.dropdown-menu-close', _this.dropdown).trigger('click'); + } + }; + })(this)); + this.dropdown.on('blur', 'a', (function(_this) { + return function(e) { + var $dropdownMenu, $relatedTarget; + if (e.relatedTarget != null) { + $relatedTarget = $(e.relatedTarget); + $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); + if ($dropdownMenu.length === 0) { + return _this.dropdown.removeClass('open'); } + } + }; + })(this)); + if (this.dropdown.find(".dropdown-toggle-page").length) { + this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.togglePage(); }; })(this)); + } + if (this.options.selectable) { + selector = ".dropdown-content a"; if (this.dropdown.find(".dropdown-toggle-page").length) { - this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { - return function(e) { - e.preventDefault(); - e.stopPropagation(); - return _this.togglePage(); - }; - })(this)); - } - if (this.options.selectable) { - selector = ".dropdown-content a"; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content a"; + selector = ".dropdown-page-one .dropdown-content a"; + } + this.dropdown.on("click", selector, function(e) { + var $el, selected, selectedObj, isMarking; + $el = $(this); + selected = self.rowClicked($el); + selectedObj = selected ? selected[0] : null; + isMarking = selected ? selected[1] : null; + if (self.options.clicked) { + self.options.clicked(selectedObj, $el, e, isMarking); } - this.dropdown.on("click", selector, function(e) { - var $el, selected, selectedObj, isMarking; - $el = $(this); - selected = self.rowClicked($el); - selectedObj = selected ? selected[0] : null; - isMarking = selected ? selected[1] : null; - if (self.options.clicked) { - self.options.clicked(selectedObj, $el, e, isMarking); - } - // Update label right after all modifications in dropdown has been done - if (self.options.toggleLabel) { - self.updateLabel(selectedObj, $el, self); - } + // Update label right after all modifications in dropdown has been done + if (self.options.toggleLabel) { + self.updateLabel(selectedObj, $el, self); + } - $el.trigger('blur'); - }); - } + $el.trigger('blur'); + }); } + } - // Finds an element inside wrapper element - GitLabDropdown.prototype.getElement = function(selector) { - return this.dropdown.find(selector); - }; + // Finds an element inside wrapper element + GitLabDropdown.prototype.getElement = function(selector) { + return this.dropdown.find(selector); + }; - GitLabDropdown.prototype.toggleLoading = function() { - return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); - }; + GitLabDropdown.prototype.toggleLoading = function() { + return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + }; - GitLabDropdown.prototype.togglePage = function() { - var menu; - menu = $('.dropdown-menu', this.dropdown); - if (menu.hasClass(PAGE_TWO_CLASS)) { - if (this.remote) { - this.remote.execute(); - } - } - menu.toggleClass(PAGE_TWO_CLASS); - // Focus first visible input on active page - return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); - }; - - GitLabDropdown.prototype.parseData = function(data) { - var full_html, groupData, html, name; - this.renderedData = data; - if (this.options.filterable && data.length === 0) { - // render no matching results - html = [this.noResults()]; - } else { - // Handle array groups - if (gl.utils.isObject(data)) { - html = []; - for (name in data) { - groupData = data[name]; - html.push(this.renderItem({ - header: name - // Add header for each group - }, name)); - this.renderData(groupData, name).map(function(item) { - return html.push(item); - }); - } - } else { - // Render each row - html = this.renderData(data); - } - } - // Render the full menu - full_html = this.renderMenu(html); - return this.appendMenu(full_html); - }; - - GitLabDropdown.prototype.renderData = function(data, group) { - if (group == null) { - group = false; + GitLabDropdown.prototype.togglePage = function() { + var menu; + menu = $('.dropdown-menu', this.dropdown); + if (menu.hasClass(PAGE_TWO_CLASS)) { + if (this.remote) { + this.remote.execute(); } - return data.map((function(_this) { - return function(obj, index) { - return _this.renderItem(obj, group, index); - }; - })(this)); - }; - - GitLabDropdown.prototype.shouldPropagate = function(e) { - var $target; - if (this.options.multiSelect) { - $target = $(e.target); - if ($target && !$target.hasClass('dropdown-menu-close') && - !$target.hasClass('dropdown-menu-close-icon') && - !$target.data('is-link')) { - e.stopPropagation(); - return false; - } else { - return true; + } + menu.toggleClass(PAGE_TWO_CLASS); + // Focus first visible input on active page + return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); + }; + + GitLabDropdown.prototype.parseData = function(data) { + var full_html, groupData, html, name; + this.renderedData = data; + if (this.options.filterable && data.length === 0) { + // render no matching results + html = [this.noResults()]; + } else { + // Handle array groups + if (gl.utils.isObject(data)) { + html = []; + for (name in data) { + groupData = data[name]; + html.push(this.renderItem({ + header: name + // Add header for each group + }, name)); + this.renderData(groupData, name).map(function(item) { + return html.push(item); + }); } + } else { + // Render each row + html = this.renderData(data); } - }; + } + // Render the full menu + full_html = this.renderMenu(html); + return this.appendMenu(full_html); + }; - GitLabDropdown.prototype.opened = function(e) { - var contentHtml; - this.resetRows(); - this.addArrowKeyEvent(); + GitLabDropdown.prototype.renderData = function(data, group) { + if (group == null) { + group = false; + } + return data.map((function(_this) { + return function(obj, index) { + return _this.renderItem(obj, group, index); + }; + })(this)); + }; - // Makes indeterminate items effective - if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - this.parseData(this.fullData); - } - contentHtml = $('.dropdown-content', this.dropdown).html(); - if (this.remote && contentHtml === "") { - this.remote.execute(); + GitLabDropdown.prototype.shouldPropagate = function(e) { + var $target; + if (this.options.multiSelect) { + $target = $(e.target); + if ($target && !$target.hasClass('dropdown-menu-close') && + !$target.hasClass('dropdown-menu-close-icon') && + !$target.data('is-link')) { + e.stopPropagation(); + return false; } else { - this.focusTextInput(); + return true; } + } + }; - if (this.options.showMenuAbove) { - this.positionMenuAbove(); - } + GitLabDropdown.prototype.opened = function(e) { + var contentHtml; + this.resetRows(); + this.addArrowKeyEvent(); - if (this.options.opened) { - this.options.opened.call(this, e); - } + // Makes indeterminate items effective + if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + this.parseData(this.fullData); + } + contentHtml = $('.dropdown-content', this.dropdown).html(); + if (this.remote && contentHtml === "") { + this.remote.execute(); + } else { + this.focusTextInput(); + } + + if (this.options.showMenuAbove) { + this.positionMenuAbove(); + } - return this.dropdown.trigger('shown.gl.dropdown'); - }; + if (this.options.opened) { + this.options.opened.call(this, e); + } - GitLabDropdown.prototype.positionMenuAbove = function() { - var $button = $(this.el); - var $menu = this.dropdown.find('.dropdown-menu'); + return this.dropdown.trigger('shown.gl.dropdown'); + }; - $menu.css('top', ($button.height() + $menu.height()) * -1); - }; + GitLabDropdown.prototype.positionMenuAbove = function() { + var $button = $(this.el); + var $menu = this.dropdown.find('.dropdown-menu'); - GitLabDropdown.prototype.hidden = function(e) { - var $input; - this.resetRows(); - this.removeArrayKeyEvent(); - $input = this.dropdown.find(".dropdown-input-field"); - if (this.options.filterable) { - $input.blur(); - } - if (this.dropdown.find(".dropdown-toggle-page").length) { - $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); - } - if (this.options.hidden) { - this.options.hidden.call(this, e); - } - return this.dropdown.trigger('hidden.gl.dropdown'); - }; + $menu.css('top', ($button.height() + $menu.height()) * -1); + }; - // Render the full menu - GitLabDropdown.prototype.renderMenu = function(html) { - if (this.options.renderMenu) { - return this.options.renderMenu(html); - } else { - var ul = document.createElement('ul'); + GitLabDropdown.prototype.hidden = function(e) { + var $input; + this.resetRows(); + this.removeArrayKeyEvent(); + $input = this.dropdown.find(".dropdown-input-field"); + if (this.options.filterable) { + $input.blur(); + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); + } + if (this.options.hidden) { + this.options.hidden.call(this, e); + } + return this.dropdown.trigger('hidden.gl.dropdown'); + }; - for (var i = 0; i < html.length; i += 1) { - var el = html[i]; + // Render the full menu + GitLabDropdown.prototype.renderMenu = function(html) { + if (this.options.renderMenu) { + return this.options.renderMenu(html); + } else { + var ul = document.createElement('ul'); - if (el instanceof jQuery) { - el = el.get(0); - } + for (var i = 0; i < html.length; i += 1) { + var el = html[i]; - if (typeof el === 'string') { - ul.innerHTML += el; - } else { - ul.appendChild(el); - } + if (el instanceof jQuery) { + el = el.get(0); } - return ul; + if (typeof el === 'string') { + ul.innerHTML += el; + } else { + ul.appendChild(el); + } } - }; - // Append the menu into the dropdown - GitLabDropdown.prototype.appendMenu = function(html) { - return this.clearMenu().append(html); - }; + return ul; + } + }; + + // Append the menu into the dropdown + GitLabDropdown.prototype.appendMenu = function(html) { + return this.clearMenu().append(html); + }; - GitLabDropdown.prototype.clearMenu = function() { - var selector; - selector = '.dropdown-content'; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; - } + GitLabDropdown.prototype.clearMenu = function() { + var selector; + selector = '.dropdown-content'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content"; + } - return $(selector, this.dropdown).empty(); - }; + return $(selector, this.dropdown).empty(); + }; - GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value; - if (group == null) { - group = false; + GitLabDropdown.prototype.renderItem = function(data, group, index) { + var field, fieldName, html, selected, text, url, value; + if (group == null) { + group = false; + } + if (index == null) { + // Render the row + index = false; + } + html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { + html.className = data; + return html; + } + // Header + if (data.header != null) { + html.className = 'dropdown-header'; + html.innerHTML = data.header; + return html; + } + if (this.options.renderRow) { + // Call the render function + html = this.options.renderRow.call(this.options, data, this); + } else { + if (!selected) { + value = this.options.id ? this.options.id(data) : data.id; + fieldName = this.options.fieldName; + + if (value) { value = value.toString().replace(/'/g, '\\\''); } + + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + if (field.length) { + selected = true; + } } - if (index == null) { - // Render the row - index = false; + // Set URL + if (this.options.url != null) { + url = this.options.url(data); + } else { + url = data.url != null ? data.url : '#'; } - html = document.createElement('li'); - if (data === 'divider' || data === 'separator') { - html.className = data; - return html; + // Set Text + if (this.options.text != null) { + text = this.options.text(data); + } else { + text = data.text != null ? data.text : ''; } - // Header - if (data.header != null) { - html.className = 'dropdown-header'; - html.innerHTML = data.header; - return html; + if (this.highlight) { + text = this.highlightTextMatches(text, this.filterInput.val()); } - if (this.options.renderRow) { - // Call the render function - html = this.options.renderRow.call(this.options, data, this); - } else { - if (!selected) { - value = this.options.id ? this.options.id(data) : data.id; - fieldName = this.options.fieldName; - - if (value) { value = value.toString().replace(/'/g, '\\\''); } - - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); - if (field.length) { - selected = true; - } - } - // Set URL - if (this.options.url != null) { - url = this.options.url(data); - } else { - url = data.url != null ? data.url : '#'; - } - // Set Text - if (this.options.text != null) { - text = this.options.text(data); - } else { - text = data.text != null ? data.text : ''; - } - if (this.highlight) { - text = this.highlightTextMatches(text, this.filterInput.val()); - } - // Create the list item & the link - var link = document.createElement('a'); - - link.href = url; - link.innerHTML = text; + // Create the list item & the link + var link = document.createElement('a'); - if (selected) { - link.className = 'is-active'; - } - - if (group) { - link.dataset.group = group; - link.dataset.index = index; - } + link.href = url; + link.innerHTML = text; - html.appendChild(link); + if (selected) { + link.className = 'is-active'; } - return html; - }; - - GitLabDropdown.prototype.highlightTextMatches = function(text, term) { - var occurrences; - occurrences = fuzzaldrinPlus.match(text, term); - return text.split('').map(function(character, i) { - if (indexOf.call(occurrences, i) >= 0) { - return "<b>" + character + "</b>"; - } else { - return character; - } - }).join(''); - }; - - GitLabDropdown.prototype.noResults = function() { - var html; - return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; - }; - - GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - - fieldName = this.options.fieldName; - isInput = $(this.el).is('input'); - if (this.renderedData) { - groupName = el.data('group'); - if (groupName) { - selectedIndex = el.data('index'); - selectedObject = this.renderedData[groupName][selectedIndex]; - } else { - selectedIndex = el.closest('li').index(); - selectedObject = this.renderedData[selectedIndex]; - } + + if (group) { + link.dataset.group = group; + link.dataset.index = index; } - if (this.options.vue) { - if (el.hasClass(ACTIVE_CLASS)) { - el.removeClass(ACTIVE_CLASS); - } else { - el.addClass(ACTIVE_CLASS); - } + html.appendChild(link); + } + return html; + }; - return [selectedObject]; + GitLabDropdown.prototype.highlightTextMatches = function(text, term) { + var occurrences; + occurrences = fuzzaldrinPlus.match(text, term); + return text.split('').map(function(character, i) { + if (indexOf.call(occurrences, i) !== -1) { + return "<b>" + character + "</b>"; + } else { + return character; } + }).join(''); + }; - field = []; - value = this.options.id - ? this.options.id(selectedObject, el) - : selectedObject.id; - if (isInput) { - field = $(this.el); - } else if (value) { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); - } + GitLabDropdown.prototype.noResults = function() { + var html; + return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>"; + }; + + GitLabDropdown.prototype.rowClicked = function(el) { + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { - return; + fieldName = this.options.fieldName; + isInput = $(this.el).is('input'); + if (this.renderedData) { + groupName = el.data('group'); + if (groupName) { + selectedIndex = el.data('index'); + selectedObject = this.renderedData[groupName][selectedIndex]; + } else { + selectedIndex = el.closest('li').index(); + selectedObject = this.renderedData[selectedIndex]; } + } + if (this.options.vue) { if (el.hasClass(ACTIVE_CLASS)) { - isMarking = false; el.removeClass(ACTIVE_CLASS); - if (field && field.length) { - this.clearField(field, isInput); - } - } else if (el.hasClass(INDETERMINATE_CLASS)) { - isMarking = true; - el.addClass(ACTIVE_CLASS); - el.removeClass(INDETERMINATE_CLASS); - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } } else { - isMarking = true; - if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { - this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); - if (!isInput) { - this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); - } - } - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); - if (value != null) { - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } else if (field && field.length) { - field.val(value).trigger('change'); - } - } } - return [selectedObject, isMarking]; - }; + return [selectedObject]; + } - GitLabDropdown.prototype.focusTextInput = function() { - if (this.options.filterable) { this.filterInput.focus(); } - }; + field = []; + value = this.options.id + ? this.options.id(selectedObject, el) + : selectedObject.id; + if (isInput) { + field = $(this.el); + } else if (value) { + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); + } - GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { - var $input; - // Create hidden input for form - $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); - if (this.options.inputId != null) { - $input.attr('id', this.options.inputId); - } - return this.dropdown.before($input); - }; - - GitLabDropdown.prototype.selectRowAtIndex = function(index) { - var $el, selector; - // If we pass an option index - if (typeof index !== "undefined") { - selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; - } else { - selector = ".dropdown-content .is-focused"; + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + + if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; + el.removeClass(ACTIVE_CLASS); + if (field && field.length) { + this.clearField(field, isInput); + } + } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; + el.addClass(ACTIVE_CLASS); + el.removeClass(INDETERMINATE_CLASS); + if (field && field.length && value == null) { + this.clearField(field, isInput); + } + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } + } else { + isMarking = true; + if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { + this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); + if (!isInput) { + this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); + } } - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + if (field && field.length && value == null) { + this.clearField(field, isInput); } - // simulate a click on the first link - $el = $(selector, this.dropdown); - if ($el.length) { - var href = $el.attr('href'); - if (href && href !== '#') { - gl.utils.visitUrl(href); - } else { - $el.first().trigger('click'); + // Toggle active class for the tick mark + el.addClass(ACTIVE_CLASS); + if (value != null) { + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } else if (field && field.length) { + field.val(value).trigger('change'); } } - }; + } - GitLabDropdown.prototype.addArrowKeyEvent = function() { - var $input, ARROW_KEY_CODES, selector; - ARROW_KEY_CODES = [38, 40]; - $input = this.dropdown.find(".dropdown-input-field"); - selector = SELECTABLE_CLASSES; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + return [selectedObject, isMarking]; + }; + + GitLabDropdown.prototype.focusTextInput = function() { + if (this.options.filterable) { this.filterInput.focus(); } + }; + + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { + var $input; + // Create hidden input for form + $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); + if (this.options.inputId != null) { + $input.attr('id', this.options.inputId); + } + return this.dropdown.before($input); + }; + + GitLabDropdown.prototype.selectRowAtIndex = function(index) { + var $el, selector; + // If we pass an option index + if (typeof index !== "undefined") { + selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; + } else { + selector = ".dropdown-content .is-focused"; + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + // simulate a click on the first link + $el = $(selector, this.dropdown); + if ($el.length) { + var href = $el.attr('href'); + if (href && href !== '#') { + gl.utils.visitUrl(href); + } else { + $el.first().trigger('click'); } - return $('body').on('keydown', (function(_this) { - return function(e) { - var $listItems, PREV_INDEX, currentKeyCode; - currentKeyCode = e.which; - if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { - e.preventDefault(); - e.stopImmediatePropagation(); - PREV_INDEX = currentIndex; - $listItems = $(selector, _this.dropdown); - // if @options.filterable - // $input.blur() - if (currentKeyCode === 40) { - // Move down - if (currentIndex < ($listItems.length - 1)) { - currentIndex += 1; - } - } else if (currentKeyCode === 38) { - // Move up - if (currentIndex > 0) { - currentIndex -= 1; - } + } + }; + + GitLabDropdown.prototype.addArrowKeyEvent = function() { + var $input, ARROW_KEY_CODES, selector; + ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find(".dropdown-input-field"); + selector = SELECTABLE_CLASSES; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + return $('body').on('keydown', (function(_this) { + return function(e) { + var $listItems, PREV_INDEX, currentKeyCode; + currentKeyCode = e.which; + if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { + e.preventDefault(); + e.stopImmediatePropagation(); + PREV_INDEX = currentIndex; + $listItems = $(selector, _this.dropdown); + // if @options.filterable + // $input.blur() + if (currentKeyCode === 40) { + // Move down + if (currentIndex < ($listItems.length - 1)) { + currentIndex += 1; } - if (currentIndex !== PREV_INDEX) { - _this.highlightRowAtIndex($listItems, currentIndex); + } else if (currentKeyCode === 38) { + // Move up + if (currentIndex > 0) { + currentIndex -= 1; } - return false; } - if (currentKeyCode === 13 && currentIndex !== -1) { - e.preventDefault(); - _this.selectRowAtIndex(); + if (currentIndex !== PREV_INDEX) { + _this.highlightRowAtIndex($listItems, currentIndex); } - }; - })(this)); - }; - - GitLabDropdown.prototype.removeArrayKeyEvent = function() { - return $('body').off('keydown'); - }; - - GitLabDropdown.prototype.resetRows = function resetRows() { - currentIndex = -1; - $('.is-focused', this.dropdown).removeClass('is-focused'); - }; - - GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { - var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; - // Remove the class for the previously focused row - $('.is-focused', this.dropdown).removeClass('is-focused'); - // Update the class for the row at the specific index - $listItem = $listItems.eq(index); - $listItem.find('a:first-child').addClass("is-focused"); - // Dropdown content scroll area - $dropdownContent = $listItem.closest('.dropdown-content'); - dropdownScrollTop = $dropdownContent.scrollTop(); - dropdownContentHeight = $dropdownContent.outerHeight(); - dropdownContentTop = $dropdownContent.prop('offsetTop'); - dropdownContentBottom = dropdownContentTop + dropdownContentHeight; - // Get the offset bottom of the list item - listItemHeight = $listItem.outerHeight(); - listItemTop = $listItem.prop('offsetTop'); - listItemBottom = listItemTop + listItemHeight; - if (!index) { - // Scroll the dropdown content to the top - $dropdownContent.scrollTop(0); - } else if (index === ($listItems.length - 1)) { - // Scroll the dropdown content to the bottom - $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); - } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { - // Scroll the dropdown content down - $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); - } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { - // Scroll the dropdown content up - return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); - } - }; + return false; + } + if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); + _this.selectRowAtIndex(); + } + }; + })(this)); + }; - GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { - if (selected == null) { - selected = null; - } - if (el == null) { - el = null; - } - if (instance == null) { - instance = null; - } - return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); - }; + GitLabDropdown.prototype.removeArrayKeyEvent = function() { + return $('body').off('keydown'); + }; - GitLabDropdown.prototype.clearField = function(field, isInput) { - return isInput ? field.val('') : field.remove(); - }; + GitLabDropdown.prototype.resetRows = function resetRows() { + currentIndex = -1; + $('.is-focused', this.dropdown).removeClass('is-focused'); + }; - return GitLabDropdown; - })(); + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { + var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + // Remove the class for the previously focused row + $('.is-focused', this.dropdown).removeClass('is-focused'); + // Update the class for the row at the specific index + $listItem = $listItems.eq(index); + $listItem.find('a:first-child').addClass("is-focused"); + // Dropdown content scroll area + $dropdownContent = $listItem.closest('.dropdown-content'); + dropdownScrollTop = $dropdownContent.scrollTop(); + dropdownContentHeight = $dropdownContent.outerHeight(); + dropdownContentTop = $dropdownContent.prop('offsetTop'); + dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + // Get the offset bottom of the list item + listItemHeight = $listItem.outerHeight(); + listItemTop = $listItem.prop('offsetTop'); + listItemBottom = listItemTop + listItemHeight; + if (!index) { + // Scroll the dropdown content to the top + $dropdownContent.scrollTop(0); + } else if (index === ($listItems.length - 1)) { + // Scroll the dropdown content to the bottom + $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); + } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + // Scroll the dropdown content down + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); + } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + // Scroll the dropdown content up + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); + } + }; - $.fn.glDropdown = function(opts) { - return this.each(function() { - if (!$.data(this, 'glDropdown')) { - return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); - } - }); + GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { + if (selected == null) { + selected = null; + } + if (el == null) { + el = null; + } + if (instance == null) { + instance = null; + } + return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); + }; + + GitLabDropdown.prototype.clearField = function(field, isInput) { + return isInput ? field.val('') : field.remove(); }; -}).call(window); + + return GitLabDropdown; +})(); + +$.fn.glDropdown = function(opts) { + return this.each(function() { + if (!$.data(this, 'glDropdown')) { + return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); + } + }); +}; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js new file mode 100644 index 00000000000..76de249ac3b --- /dev/null +++ b/app/assets/javascripts/gl_field_error.js @@ -0,0 +1,162 @@ +/** + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + * <form class='gl-show-field-errors'> + * <input type='text' name='username' title='Username is required.'/> + * </form> + * + * Ignore specific inputs (e.g. UsernameValidator): + * + * <form class='gl-show-field-errors'> + * <div class="form-group> + * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/> + * </div> + * <div class="form-group"> + * <input type='text' name='username' title='Username is required.'/> + * </div> + * </form> + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + * <form class='gl-show-field-errors'> + * <div class="form-group gl-field-error-anchor"> + * <input type='text' name='username' title='Username is required.'/> + * // Error message typically injected here + * </div> + * // Error message now injected here + * </form> + * + */ + +/** + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + */ + +const errorMessageClass = 'gl-field-error'; +const inputErrorClass = 'gl-field-error-outline'; +const errorAnchorSelector = '.gl-field-error-anchor'; +const ignoreInputSelector = '.gl-field-error-ignore'; + +class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); + } + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } +} + +window.gl = window.gl || {}; +window.gl.GlFieldError = GlFieldError; diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6 deleted file mode 100644 index f7cbecc0385..00000000000 --- a/app/assets/javascripts/gl_field_error.js.es6 +++ /dev/null @@ -1,164 +0,0 @@ -/* eslint-disable no-param-reassign */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `gl-show-field-errors` to the form element, and ensure error messages are - * declared in each inputs' `title` attribute. If no title is declared for an invalid - * field the user attempts to submit, "This field is required." will be shown by default. - * - * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. - * - * Set a custom error anchor for error message to be injected after with the - * class `gl-field-error-anchor` - * - * Examples: - * - * Basic: - * - * <form class='gl-show-field-errors'> - * <input type='text' name='username' title='Username is required.'/> - * </form> - * - * Ignore specific inputs (e.g. UsernameValidator): - * - * <form class='gl-show-field-errors'> - * <div class="form-group> - * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/> - * </div> - * <div class="form-group"> - * <input type='text' name='username' title='Username is required.'/> - * </div> - * </form> - * - * Custom Error Anchor (allows error message to be injected after specified element): - * - * <form class='gl-show-field-errors'> - * <div class="form-group gl-field-error-anchor"> - * <input type='text' name='username' title='Username is required.'/> - * // Error message typically injected here - * </div> - * // Error message now injected here - * </form> - * - * */ - - /* - * Regex Patterns in use: - * - * Only alphanumeric: : "[a-zA-Z0-9]+" - * No special characters : "[a-zA-Z0-9-_]+", - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - const errorAnchorSelector = '.gl-field-error-anchor'; - const ignoreInputSelector = '.gl-field-error-ignore'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); - - this.state = { - valid: false, - empty: true, - }; - - this.initFieldValidation(); - } - - initFieldValidation() { - const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); - const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; - - // hidden when injected into DOM - errorAnchor.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } - - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); - - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } - - renderValidity() { - this.renderClear(); - - if (this.state.valid) { - this.renderValid(); - } else if (this.state.empty) { - this.renderEmpty(); - } else if (!this.state.valid) { - this.renderInvalid(); - } - } - - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') - .on('keyup.fieldValidator', this.updateValidity.bind(this)); - } - - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } - - getInputValidity() { - return this.inputDomElement.validity.valid; - } - - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } - - renderValid() { - return this.renderClear(); - } - - renderEmpty() { - return this.renderInvalid(); - } - - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } - - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); - } - } - - global.GlFieldError = GlFieldError; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js new file mode 100644 index 00000000000..636258ec555 --- /dev/null +++ b/app/assets/javascripts/gl_field_errors.js @@ -0,0 +1,47 @@ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ + +require('./gl_field_error'); + +const customValidationFlag = 'gl-field-error-ignore'; + +class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } + + initValidators () { + // register selectors here as needed + const validateSelectors = [':text', ':password', '[type=email]'] + .map((selector) => `input${selector}`).join(','); + + this.state.inputs = this.form.find(validateSelectors).toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new window.gl.GlFieldError({ input, formErrors: this })); + + this.form.on('submit', this.catchInvalidFormSubmit); + } + + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ + + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + } + + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); + } +} + +window.gl = window.gl || {}; +window.gl.GlFieldErrors = GlFieldErrors; diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 deleted file mode 100644 index e9add115429..00000000000 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ - -require('./gl_field_error'); - -((global) => { - const customValidationFlag = 'gl-field-error-ignore'; - - class GlFieldErrors { - constructor(form) { - this.form = $(form); - this.state = { - inputs: [], - valid: false - }; - this.initValidators(); - } - - initValidators () { - // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] - .map((selector) => `input${selector}`).join(','); - - this.state.inputs = this.form.find(validateSelectors).toArray() - .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new global.GlFieldError({ input, formErrors: this })); - - this.form.on('submit', this.catchInvalidFormSubmit); - } - - /* Neccessary to prevent intercept and override invalid form submit - * because Safari & iOS quietly allow form submission when form is invalid - * and prevents disabling of invalid submit button by application.js */ - - catchInvalidFormSubmit (event) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); - } - } - - focusOnFirstInvalid () { - const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; - firstInvalid.inputElement.focus(); - } - } - - global.GlFieldErrors = GlFieldErrors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js new file mode 100644 index 00000000000..e7c98e16581 --- /dev/null +++ b/app/assets/javascripts/gl_form.js @@ -0,0 +1,90 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + +window.gl = window.gl || {}; + +function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); +} + +GLForm.prototype.destroy = function() { + // Clean form listeners + this.clearEventListeners(); + return this.form.data('gl-form', null); +}; + +GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + // remove notify commit author checkbox for non-commit notes + gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new DropzoneInput(this.form); + autosize(this.textarea); + // form and textarea event listeners + this.addEventListeners(); + } + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); +}; + +GLForm.prototype.setupAutosize = function () { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); + + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); + + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); +}; + +GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); +}; + +GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); + + if (this.textarea.data('height') === outerHeight) return; + + autosize.destroy(this.textarea); + + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); +}; + +GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); +}; + +GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); +}; + +window.gl.GLForm = GLForm; diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6 deleted file mode 100644 index 0b446ff364a..00000000000 --- a/app/assets/javascripts/gl_form.js.es6 +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ -/* global DropzoneInput */ -/* global autosize */ - -(() => { - const global = window.gl || (window.gl = {}); - - function GLForm(form) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); - } - - GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - return this.form.data('gl-form', null); - }; - - GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - this.form.addClass('gfm-form'); - // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); - new DropzoneInput(this.form); - autosize(this.textarea); - // form and textarea event listeners - this.addEventListeners(); - } - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - this.form.show(); - if (this.isAutosizeable) this.setupAutosize(); - }; - - GLForm.prototype.setupAutosize = function () { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); - - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); - - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); - }; - - GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); - }; - - GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); - - if (this.textarea.data('height') === outerHeight) return; - - autosize.destroy(this.textarea); - - this.textarea.data('height', outerHeight); - this.textarea.outerHeight(outerHeight); - this.textarea.css('max-height', window.outerHeight); - }; - - GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); - }; - - GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); - }; - - global.GLForm = GLForm; -})(); diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 4f7777aa5bc..a433c7ba8f0 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,3 +1,6 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/)); +import Chart from 'vendor/Chart'; +import ContributorsStatGraph from './stat_graph_contributors'; + +// export to global scope +window.Chart = Chart; +window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js deleted file mode 100644 index 75a53aae33c..00000000000 --- a/app/assets/javascripts/graphs/stat_graph.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */ -(function() { - this.StatGraph = (function() { - function StatGraph() {} - - StatGraph.log = {}; - - StatGraph.get_log = function() { - return this.log; - }; - - StatGraph.set_log = function(data) { - return this.log = data; - }; - - return StatGraph; - })(); -}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index bbfb467ad50..c6be4c9e8fe 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,116 +1,111 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ -/* global ContributorsGraph */ -/* global ContributorsAuthorGraph */ -/* global ContributorsMasterGraph */ -/* global ContributorsStatGraphUtil */ -/* global d3 */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ -window.d3 = require('d3'); +import d3 from 'd3'; +import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; +import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -(function() { - this.ContributorsStatGraph = (function() { - function ContributorsStatGraph() {} +export default (function() { + function ContributorsStatGraph() {} - ContributorsStatGraph.prototype.init = function(log) { - var author_commits, total_commits; - this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); - total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); - this.add_master_graph(total_commits); - this.add_authors_graph(author_commits); - return this.change_date_header(); - }; + ContributorsStatGraph.prototype.init = function(log) { + var author_commits, total_commits; + this.parsed_log = ContributorsStatGraphUtil.parse_log(log); + this.set_current_field("commits"); + total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); + this.add_master_graph(total_commits); + this.add_authors_graph(author_commits); + return this.change_date_header(); + }; - ContributorsStatGraph.prototype.add_master_graph = function(total_data) { - this.master_graph = new ContributorsMasterGraph(total_data); - return this.master_graph.draw(); - }; + ContributorsStatGraph.prototype.add_master_graph = function(total_data) { + this.master_graph = new ContributorsMasterGraph(total_data); + return this.master_graph.draw(); + }; - ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { - var limited_author_data; - this.authors = []; - limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); - return author_graph.draw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { + var limited_author_data; + this.authors = []; + limited_author_data = author_data.slice(0, 100); + return _.each(limited_author_data, (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $(".contributors-list").append(author_header); + _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + return author_graph.draw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.format_author_commit_info = function(author) { - var commits; - commits = $('<span/>', { - "class": 'graph-author-commits-count' - }); - commits.text(author.commits + " commits"); - return $('<span/>').append(commits); - }; + ContributorsStatGraph.prototype.format_author_commit_info = function(author) { + var commits; + commits = $('<span/>', { + "class": 'graph-author-commits-count' + }); + commits.text(author.commits + " commits"); + return $('<span/>').append(commits); + }; - ContributorsStatGraph.prototype.create_author_header = function(author) { - var author_commit_info, author_commit_info_span, author_email, author_name, list_item; - list_item = $('<li/>', { - "class": 'person', - style: 'display: block;' - }); - author_name = $('<h4>' + author.author_name + '</h4>'); - author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); - author_commit_info_span = $('<span/>', { - "class": 'commits' - }); - author_commit_info = this.format_author_commit_info(author); - author_commit_info_span.html(author_commit_info); - list_item.append(author_name); - list_item.append(author_email); - list_item.append(author_commit_info_span); - return list_item; - }; + ContributorsStatGraph.prototype.create_author_header = function(author) { + var author_commit_info, author_commit_info_span, author_email, author_name, list_item; + list_item = $('<li/>', { + "class": 'person', + style: 'display: block;' + }); + author_name = $('<h4>' + author.author_name + '</h4>'); + author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); + author_commit_info_span = $('<span/>', { + "class": 'commits' + }); + author_commit_info = this.format_author_commit_info(author); + author_commit_info_span.html(author_commit_info); + list_item.append(author_name); + list_item.append(author_email); + list_item.append(author_commit_info_span); + return list_item; + }; - ContributorsStatGraph.prototype.redraw_master = function() { - var total_data; - total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); - this.master_graph.set_data(total_data); - return this.master_graph.redraw(); - }; + ContributorsStatGraph.prototype.redraw_master = function() { + var total_data; + total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); + this.master_graph.set_data(total_data); + return this.master_graph.redraw(); + }; - ContributorsStatGraph.prototype.redraw_authors = function() { - var author_commits, x_domain; - $("ol").html(""); - x_domain = ContributorsGraph.prototype.x_domain; - author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - }; - })(this)); - }; + ContributorsStatGraph.prototype.redraw_authors = function() { + var author_commits, x_domain; + $("ol").html(""); + x_domain = ContributorsGraph.prototype.x_domain; + author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); + return _.each(author_commits, (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + $(_this.authors[d.author_name].list_item).appendTo("ol"); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + }; + })(this)); + }; - ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; - }; + ContributorsStatGraph.prototype.set_current_field = function(field) { + return this.field = field; + }; - ContributorsStatGraph.prototype.change_date_header = function() { - var print, print_date_format, x_domain; - x_domain = ContributorsGraph.prototype.x_domain; - print_date_format = d3.time.format("%B %e %Y"); - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); - return $("#date_header").text(print); - }; + ContributorsStatGraph.prototype.change_date_header = function() { + var print, print_date_format, x_domain; + x_domain = ContributorsGraph.prototype.x_domain; + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + return $("#date_header").text(print); + }; - ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { - var author_commit_info, author_list_item; - author_list_item = $(this.authors[author.author_name].list_item); - author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); - }; + ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { + var author_commit_info, author_list_item; + author_list_item = $(this.authors[author.author_name].list_item); + author_commit_info = this.format_author_commit_info(author); + return author_list_item.find("span").html(author_commit_info); + }; - return ContributorsStatGraph; - })(); -}).call(window); + return ContributorsStatGraph; +})(); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 228771da4ee..521bc77db66 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,276 +1,272 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */ -/* global d3 */ -/* global ContributorsGraph */ - -window.d3 = require('d3'); - -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ContributorsGraph = (function() { - function ContributorsGraph() {} - - ContributorsGraph.prototype.MARGIN = { - top: 20, - right: 20, - bottom: 30, - left: 50 - }; - - ContributorsGraph.prototype.x_domain = null; - - ContributorsGraph.prototype.y_domain = null; - - ContributorsGraph.prototype.dates = []; - - ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; - }; - - ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { - return d.date; - }); - }; - - ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; - }; - - ContributorsGraph.init_domain = function(data) { - ContributorsGraph.init_x_domain(data); - return ContributorsGraph.init_y_domain(data); - }; - - ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; - }; - - ContributorsGraph.prototype.set_x_domain = function() { - return this.x.domain(this.x_domain); - }; - - ContributorsGraph.prototype.set_y_domain = function() { - return this.y.domain(this.y_domain); - }; - - ContributorsGraph.prototype.set_domain = function() { - this.set_x_domain(); - return this.set_y_domain(); - }; - - ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.time.scale().range([0, width]).clamp(true); - return this.y = d3.scale.linear().range([height, 0]).nice(); - }; - - ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); - }; - - ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); - }; - - ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; - }; - - return ContributorsGraph; - })(); - - this.ContributorsMasterGraph = (function(superClass) { - extend(ContributorsMasterGraph, superClass); - - function ContributorsMasterGraph(data1) { - this.data = data1; - this.update_content = bind(this.update_content, this); - this.width = $('.content').width() - 70; - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.brush = null; - this.x_max_domain = null; +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ + +import d3 from 'd3'; + +const bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const hasProp = {}.hasOwnProperty; + +export const ContributorsGraph = (function() { + function ContributorsGraph() {} + + ContributorsGraph.prototype.MARGIN = { + top: 20, + right: 20, + bottom: 30, + left: 50 + }; + + ContributorsGraph.prototype.x_domain = null; + + ContributorsGraph.prototype.y_domain = null; + + ContributorsGraph.prototype.dates = []; + + ContributorsGraph.set_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = data; + }; + + ContributorsGraph.set_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_x_domain = function(data) { + return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return d.date; + }); + }; + + ContributorsGraph.init_y_domain = function(data) { + return ContributorsGraph.prototype.y_domain = [ + 0, d3.max(data, function(d) { + return d.commits = d.commits || d.additions || d.deletions; + }) + ]; + }; + + ContributorsGraph.init_domain = function(data) { + ContributorsGraph.init_x_domain(data); + return ContributorsGraph.init_y_domain(data); + }; + + ContributorsGraph.set_dates = function(data) { + return ContributorsGraph.prototype.dates = data; + }; + + ContributorsGraph.prototype.set_x_domain = function() { + return this.x.domain(this.x_domain); + }; + + ContributorsGraph.prototype.set_y_domain = function() { + return this.y.domain(this.y_domain); + }; + + ContributorsGraph.prototype.set_domain = function() { + this.set_x_domain(); + return this.set_y_domain(); + }; + + ContributorsGraph.prototype.create_scale = function(width, height) { + this.x = d3.time.scale().range([0, width]).clamp(true); + return this.y = d3.scale.linear().range([height, 0]).nice(); + }; + + ContributorsGraph.prototype.draw_x_axis = function() { + return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + }; + + ContributorsGraph.prototype.draw_y_axis = function() { + return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + }; + + ContributorsGraph.prototype.set_data = function(data) { + return this.data = data; + }; + + return ContributorsGraph; +})(); + +export const ContributorsMasterGraph = (function(superClass) { + extend(ContributorsMasterGraph, superClass); + + function ContributorsMasterGraph(data1) { + this.data = data1; + this.update_content = bind(this.update_content, this); + this.width = $('.content').width() - 70; + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.brush = null; + this.x_max_domain = null; + } + + ContributorsMasterGraph.prototype.process_dates = function(data) { + var dates; + dates = this.get_dates(data); + this.parse_dates(data); + return ContributorsGraph.set_dates(dates); + }; + + ContributorsMasterGraph.prototype.get_dates = function(data) { + return _.pluck(data, 'date'); + }; + + ContributorsMasterGraph.prototype.parse_dates = function(data) { + var parseDate; + parseDate = d3.time.format("%Y-%m-%d").parse; + return data.forEach(function(d) { + return d.date = parseDate(d.date); + }); + }; + + ContributorsMasterGraph.prototype.create_scale = function() { + return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsMasterGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsMasterGraph.prototype.create_svg = function() { + return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsMasterGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { + return x(d.date); + }).y0(this.height).y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + }).interpolate("basis"); + }; + + ContributorsMasterGraph.prototype.create_brush = function() { + return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); + }; + + ContributorsMasterGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + }; + + ContributorsMasterGraph.prototype.add_brush = function() { + return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + }; + + ContributorsMasterGraph.prototype.update_content = function() { + ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); + return $("#brush_change").trigger('change'); + }; + + ContributorsMasterGraph.prototype.draw = function() { + this.process_dates(this.data); + this.create_scale(); + this.create_axes(); + ContributorsGraph.init_domain(this.data); + this.x_max_domain = this.x_domain; + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.create_brush(); + this.draw_path(this.data); + this.draw_x_axis(); + this.draw_y_axis(); + return this.add_brush(); + }; + + ContributorsMasterGraph.prototype.redraw = function() { + this.process_dates(this.data); + ContributorsGraph.set_y_domain(this.data); + this.set_y_domain(); + this.svg.select("path").datum(this.data); + this.svg.select("path").attr("d", this.area); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsMasterGraph; +})(ContributorsGraph); + +export const ContributorsAuthorGraph = (function(superClass) { + extend(ContributorsAuthorGraph, superClass); + + function ContributorsAuthorGraph(data1) { + this.data = data1; + // Don't split graph size in half for mobile devices. + if ($(window).width() < 768) { + this.width = $('.content').width() - 80; + } else { + this.width = ($('.content').width() / 2) - 100; } - - ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); - this.parse_dates(data); - return ContributorsGraph.set_dates(dates); - }; - - ContributorsMasterGraph.prototype.get_dates = function(data) { - return _.pluck(data, 'date'); - }; - - ContributorsMasterGraph.prototype.parse_dates = function(data) { + this.height = 200; + this.x = null; + this.y = null; + this.x_axis = null; + this.y_axis = null; + this.area = null; + this.svg = null; + this.list_item = null; + } + + ContributorsAuthorGraph.prototype.create_scale = function() { + return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); + }; + + ContributorsAuthorGraph.prototype.create_axes = function() { + this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); + }; + + ContributorsAuthorGraph.prototype.create_area = function(x, y) { + return this.area = d3.svg.area().x(function(d) { var parseDate; parseDate = d3.time.format("%Y-%m-%d").parse; - return data.forEach(function(d) { - return d.date = parseDate(d.date); - }); - }; - - ContributorsMasterGraph.prototype.create_scale = function() { - return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }).interpolate("basis"); - }; - - ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); - }; - - ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); - }; - - ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); - }; - - ContributorsMasterGraph.prototype.update_content = function() { - ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); - return $("#brush_change").trigger('change'); - }; - - ContributorsMasterGraph.prototype.draw = function() { - this.process_dates(this.data); - this.create_scale(); - this.create_axes(); - ContributorsGraph.init_domain(this.data); - this.x_max_domain = this.x_domain; - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.create_brush(); - this.draw_path(this.data); - this.draw_x_axis(); - this.draw_y_axis(); - return this.add_brush(); - }; - - ContributorsMasterGraph.prototype.redraw = function() { - this.process_dates(this.data); - ContributorsGraph.set_y_domain(this.data); - this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsMasterGraph; - })(ContributorsGraph); - - this.ContributorsAuthorGraph = (function(superClass) { - extend(ContributorsAuthorGraph, superClass); - - function ContributorsAuthorGraph(data1) { - this.data = data1; - // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; - } else { - this.width = ($('.content').width() / 2) - 100; - } - this.height = 200; - this.x = null; - this.y = null; - this.x_axis = null; - this.y_axis = null; - this.area = null; - this.svg = null; - this.list_item = null; - } - - ContributorsAuthorGraph.prototype.create_scale = function() { - return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height); - }; - - ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); - return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); - }; - - ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.svg.area().x(function(d) { - var parseDate; - parseDate = d3.time.format("%Y-%m-%d").parse; - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)).interpolate("basis"); - }; - - ContributorsAuthorGraph.prototype.create_svg = function() { - this.list_item = d3.selectAll(".person")[0].pop(); - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); - }; - - ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); - }; - - ContributorsAuthorGraph.prototype.draw = function() { - this.create_scale(); - this.create_axes(); - this.set_domain(); - this.create_area(this.x, this.y); - this.create_svg(); - this.draw_path(this.dates); - this.draw_x_axis(); - return this.draw_y_axis(); - }; - - ContributorsAuthorGraph.prototype.redraw = function() { - this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); - }; - - return ContributorsAuthorGraph; - })(ContributorsGraph); -}).call(window); + return x(parseDate(d)); + }).y0(this.height).y1((function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this)).interpolate("basis"); + }; + + ContributorsAuthorGraph.prototype.create_svg = function() { + this.list_item = d3.selectAll(".person")[0].pop(); + return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + }; + + ContributorsAuthorGraph.prototype.draw_path = function(data) { + return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + }; + + ContributorsAuthorGraph.prototype.draw = function() { + this.create_scale(); + this.create_axes(); + this.set_domain(); + this.create_area(this.x, this.y); + this.create_svg(); + this.draw_path(this.dates); + this.draw_x_axis(); + return this.draw_y_axis(); + }; + + ContributorsAuthorGraph.prototype.redraw = function() { + this.set_domain(); + this.svg.select("path").datum(this.dates); + this.svg.select("path").attr("d", this.area); + this.svg.select(".x.axis").call(this.x_axis); + return this.svg.select(".y.axis").call(this.y_axis); + }; + + return ContributorsAuthorGraph; +})(ContributorsGraph); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 7954c583598..c583757f3f2 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,138 +1,137 @@ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ -(function() { - window.ContributorsStatGraphUtil = { - parse_log: function(log) { - var by_author, by_email, data, entry, i, len, total, normalized_email; - total = {}; - by_author = {}; - by_email = {}; - for (i = 0, len = log.length; i < len; i += 1) { - entry = log[i]; - if (total[entry.date] == null) { - this.add_date(entry.date, total); - } - normalized_email = entry.author_email.toLowerCase(); - data = by_author[entry.author_name] || by_email[normalized_email]; - if (data == null) { - data = this.add_author(entry, by_author, by_email); - } - if (!data[entry.date]) { - this.add_date(entry.date, data); - } - this.store_data(entry, total[entry.date], data[entry.date]); - } - total = _.toArray(total); - by_author = _.toArray(by_author); - return { - total: total, - by_author: by_author - }; - }, - add_date: function(date, collection) { - collection[date] = {}; - return collection[date].date = date; - }, - add_author: function(author, by_author, by_email) { - var data, normalized_email; - data = {}; - data.author_name = author.author_name; - data.author_email = author.author_email; - normalized_email = author.author_email.toLowerCase(); - by_author[author.author_name] = data; - by_email[normalized_email] = data; - return data; - }, - store_data: function(entry, total, by_author) { - this.store_commits(total, by_author); - this.store_additions(entry, total, by_author); - return this.store_deletions(entry, total, by_author); - }, - store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); - }, - add: function(collection, field, value) { - if (collection[field] == null) { - collection[field] = 0; - } - return collection[field] += value; - }, - store_additions: function(entry, total, by_author) { - if (entry.additions == null) { - entry.additions = 0; + +export default { + parse_log: function(log) { + var by_author, by_email, data, entry, i, len, total, normalized_email; + total = {}; + by_author = {}; + by_email = {}; + for (i = 0, len = log.length; i < len; i += 1) { + entry = log[i]; + if (total[entry.date] == null) { + this.add_date(entry.date, total); } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); - }, - store_deletions: function(entry, total, by_author) { - if (entry.deletions == null) { - entry.deletions = 0; + normalized_email = entry.author_email.toLowerCase(); + data = by_author[entry.author_name] || by_email[normalized_email]; + if (data == null) { + data = this.add_author(entry, by_author, by_email); } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); - }, - get_total_data: function(parsed_log, field) { - var log, total_data; - log = parsed_log.total; - total_data = this.pick_field(log, field); - return _.sortBy(total_data, function(d) { - return d.date; - }); - }, - pick_field: function(log, field) { - var total_data; - total_data = []; - _.each(log, function(d) { - return total_data.push(_.pick(d, [field, 'date'])); - }); - return total_data; - }, - get_author_data: function(parsed_log, field, date_range) { - var author_data, log; - if (date_range == null) { - date_range = null; - } - log = parsed_log.by_author; - author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); - return _.sortBy(author_data, function(d) { - return d[field]; - }).reverse(); - }, - parse_log_entry: function(log_entry, field, date_range) { - var parsed_entry; - parsed_entry = {}; - parsed_entry.author_name = log_entry.author_name; - parsed_entry.author_email = log_entry.author_email; - parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); - return parsed_entry; - }, - in_range: function(date, date_range) { - var ref; - if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { - return true; - } else { - return false; + if (!data[entry.date]) { + this.add_date(entry.date, data); } + this.store_data(entry, total[entry.date], data[entry.date]); + } + total = _.toArray(total); + by_author = _.toArray(by_author); + return { + total: total, + by_author: by_author + }; + }, + add_date: function(date, collection) { + collection[date] = {}; + return collection[date].date = date; + }, + add_author: function(author, by_author, by_email) { + var data, normalized_email; + data = {}; + data.author_name = author.author_name; + data.author_email = author.author_email; + normalized_email = author.author_email.toLowerCase(); + by_author[author.author_name] = data; + by_email[normalized_email] = data; + return data; + }, + store_data: function(entry, total, by_author) { + this.store_commits(total, by_author); + this.store_additions(entry, total, by_author); + return this.store_deletions(entry, total, by_author); + }, + store_commits: function(total, by_author) { + this.add(total, "commits", 1); + return this.add(by_author, "commits", 1); + }, + add: function(collection, field, value) { + if (collection[field] == null) { + collection[field] = 0; + } + return collection[field] += value; + }, + store_additions: function(entry, total, by_author) { + if (entry.additions == null) { + entry.additions = 0; + } + this.add(total, "additions", entry.additions); + return this.add(by_author, "additions", entry.additions); + }, + store_deletions: function(entry, total, by_author) { + if (entry.deletions == null) { + entry.deletions = 0; + } + this.add(total, "deletions", entry.deletions); + return this.add(by_author, "deletions", entry.deletions); + }, + get_total_data: function(parsed_log, field) { + var log, total_data; + log = parsed_log.total; + total_data = this.pick_field(log, field); + return _.sortBy(total_data, function(d) { + return d.date; + }); + }, + pick_field: function(log, field) { + var total_data; + total_data = []; + _.each(log, function(d) { + return total_data.push(_.pick(d, [field, 'date'])); + }); + return total_data; + }, + get_author_data: function(parsed_log, field, date_range) { + var author_data, log; + if (date_range == null) { + date_range = null; + } + log = parsed_log.by_author; + author_data = []; + _.each(log, (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this)); + return _.sortBy(author_data, function(d) { + return d[field]; + }).reverse(); + }, + parse_log_entry: function(log_entry, field, date_range) { + var parsed_entry; + parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; + parsed_entry.author_email = log_entry.author_email; + parsed_entry.dates = {}; + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return parsed_entry.deletions += value.deletions; + } + }; + })(this)); + return parsed_entry; + }, + in_range: function(date, date_range) { + var ref; + if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) { + return true; + } else { + return false; } - }; -}).call(window); + } +}; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index c5cb273c5b2..f03b47b1c1d 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,20 +1,19 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ -(function() { - this.GroupAvatar = (function() { - function GroupAvatar() { - $('.js-choose-group-avatar-button').on("click", function() { - var form; - form = $(this).closest("form"); - return form.find(".js-group-avatar-input").click(); - }); - $('.js-group-avatar-input').on("change", function() { - var filename, form; - form = $(this).closest("form"); - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find(".js-avatar-filename").text(filename); - }); - } - return GroupAvatar; - })(); -}).call(window); +window.GroupAvatar = (function() { + function GroupAvatar() { + $('.js-choose-group-avatar-button').on("click", function() { + var form; + form = $(this).closest("form"); + return form.find(".js-group-avatar-input").click(); + }); + $('.js-group-avatar-input').on("change", function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find(".js-avatar-filename").text(filename); + }); + } + + return GroupAvatar; +})(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js new file mode 100644 index 00000000000..7dc9ce898e8 --- /dev/null +++ b/app/assets/javascripts/group_label_subscription.js @@ -0,0 +1,52 @@ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ + +class GroupLabelSubscription { + constructor(container) { + const $container = $(container); + this.$dropdown = $container.find('.dropdown'); + this.$subscribeButtons = $container.find('.js-subscribe-button'); + this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); + + this.$subscribeButtons.on('click', this.subscribe.bind(this)); + this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); + } + + unsubscribe(event) { + event.preventDefault(); + + const url = this.$unsubscribeButtons.attr('data-url'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }); + } + + subscribe(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const url = $btn.attr('data-url'); + + this.$unsubscribeButtons.attr('data-url', url); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + }); + } + + toggleSubscriptionButtons() { + this.$dropdown.toggleClass('hidden'); + this.$subscribeButtons.toggleClass('hidden'); + this.$unsubscribeButtons.toggleClass('hidden'); + } +} + +window.gl = window.gl || {}; +window.gl.GroupLabelSubscription = GroupLabelSubscription; diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 deleted file mode 100644 index 15e695e81cf..00000000000 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ - -(function(global) { - class GroupLabelSubscription { - constructor(container) { - const $container = $(container); - this.$dropdown = $container.find('.dropdown'); - this.$subscribeButtons = $container.find('.js-subscribe-button'); - this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); - - this.$subscribeButtons.on('click', this.subscribe.bind(this)); - this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); - } - - unsubscribe(event) { - event.preventDefault(); - - const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); - } - - subscribe(event) { - event.preventDefault(); - - const $btn = $(event.currentTarget); - const url = $btn.attr('data-url'); - - this.$unsubscribeButtons.attr('data-url', url); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - }); - } - - toggleSubscriptionButtons() { - this.$dropdown.toggleClass('hidden'); - this.$subscribeButtons.toggleClass('hidden'); - this.$unsubscribeButtons.toggleClass('hidden'); - } - } - - global.GroupLabelSubscription = GroupLabelSubscription; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js new file mode 100644 index 00000000000..6a028f299b1 --- /dev/null +++ b/app/assets/javascripts/group_name.js @@ -0,0 +1,40 @@ +const GROUP_LIMIT = 2; + +export default class GroupName { + constructor() { + this.titleContainer = document.querySelector('.title'); + this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = document.querySelector('.group-title'); + this.toggle = null; + this.isHidden = false; + this.init(); + } + + init() { + if (this.groups.length > GROUP_LIMIT) { + this.groups[this.groups.length - 1].classList.remove('hidable'); + this.addToggle(); + } + this.render(); + } + + addToggle() { + const header = document.querySelector('.header-content'); + this.toggle = document.createElement('button'); + this.toggle.className = 'text-expander group-name-toggle'; + this.toggle.setAttribute('aria-label', 'Toggle full path'); + this.toggle.innerHTML = '...'; + this.toggle.addEventListener('click', this.toggleGroups.bind(this)); + header.insertBefore(this.toggle, this.titleContainer); + this.toggleGroups(); + } + + toggleGroups() { + this.isHidden = !this.isHidden; + this.groupTitle.classList.toggle('is-hidden'); + } + + render() { + this.titleContainer.classList.remove('initializing'); + } +} diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups_list.js new file mode 100644 index 00000000000..56a8cbf6d03 --- /dev/null +++ b/app/assets/javascripts/groups_list.js @@ -0,0 +1,18 @@ +import FilterableList from './filterable_list'; + +/** + * Makes search request for groups when user types a value in the search input. + * Updates the html content of the page with the received one. + */ +export default class GroupsList { + constructor() { + const form = document.querySelector('form#group-filter-form'); + const filter = document.querySelector('.js-groups-list-filter'); + const holder = document.querySelector('.js-groups-list-holder'); + + if (form && filter && holder) { + const list = new FilterableList(form, filter, holder); + list.initSearch(); + } + } +} diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 6b937e7fa0f..e5dfa30edab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,71 +1,69 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* global Api */ -(function() { - var slice = [].slice; +var slice = [].slice; - this.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - return function(i, select) { - var all_available, skip_groups; - all_available = $(select).data('all-available'); - skip_groups = $(select).data('skip-groups') || []; - return $(select).select2({ - placeholder: "Search for a group", - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - var options = { all_available: all_available, skip_groups: skip_groups }; - return Api.groups(query.term, options, function(groups) { - var data; - data = { - results: groups - }; - return query.callback(data); - }); - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: "ajax-groups-dropdown", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; +window.GroupsSelect = (function() { + function GroupsSelect() { + $('.ajax-groups-select').each((function(_this) { + return function(i, select) { + var all_available, skip_groups; + all_available = $(select).data('all-available'); + skip_groups = $(select).data('skip-groups') || []; + return $(select).select2({ + placeholder: "Search for a group", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + var options = { all_available: all_available, skip_groups: skip_groups }; + return Api.groups(query.term, options, function(groups) { + var data; + data = { + results: groups + }; + return query.callback(data); + }); + }, + initSelection: function(element, callback) { + var id; + id = $(element).val(); + if (id !== "") { + return Api.group(id, callback); } - }); - }; - })(this)); - } + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-groups-dropdown", + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; - }; + GroupsSelect.prototype.formatResult = function(group) { + var avatar; + if (group.avatar_url) { + avatar = group.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; + }; - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + GroupsSelect.prototype.formatSelection = function(group) { + return group.full_name; + }; - return GroupsSelect; - })(); -}).call(window); + return GroupsSelect; +})(); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index a853c3aeb1f..34f44dad7a5 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,8 +1,7 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */ -(function() { - $(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-pending-count'); - $todoPendingCount.text(gl.text.highCountTrim(count)); - $todoPendingCount.toggleClass('hidden', count === 0); - }); -})(); +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ + +$(document).on('todo:toggle', function(e, count) { + var $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(gl.text.highCountTrim(count)); + $todoPendingCount.toggleClass('hidden', count === 0); +}); diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js new file mode 100644 index 00000000000..3bfce32768a --- /dev/null +++ b/app/assets/javascripts/issuable.js @@ -0,0 +1,188 @@ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ +/* global Issuable */ + +((global) => { + var issuable_created; + + issuable_created = false; + + global.Issuable = { + init: function() { + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + Issuable.initResetFilters(); + Issuable.resetIncomingEmailToken(); + return Issuable.initLabelFilterRemove(); + }, + initTemplates: function() { + return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); + }, + initSearch: function() { + const $searchInput = $('#issuable_search'); + + Issuable.initSearchState($searchInput); + + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); + + $searchInput.off('keyup').on('keyup', debouncedExecSearch); + + // ensures existing filters are preserved when manually submitted + $('#issuable_search_form').on('submit', (e) => { + e.preventDefault(); + debouncedExecSearch(e); + }); + }, + initSearchState: function($searchInput) { + const currentSearchVal = $searchInput.val(); + + Issuable.searchState = { + elem: $searchInput, + current: currentSearchVal + }; + + Issuable.maybeFocusOnSearch(); + }, + accessSearchPristine: function(set) { + // store reference to previous value to prevent search on non-mutating keyup + const state = Issuable.searchState; + const currentSearchVal = state.elem.val(); + + if (set) { + state.current = currentSearchVal; + } else { + return state.current === currentSearchVal; + } + }, + maybeFocusOnSearch: function() { + const currentSearchVal = Issuable.searchState.current; + if (currentSearchVal && currentSearchVal !== '') { + const queryLength = currentSearchVal.length; + const $searchInput = Issuable.searchState.elem; + + /* The following ensures that the cursor is initially placed at + * the end of search input when focus is applied. It accounts + * for differences in browser implementations of `setSelectionRange` + * and cursor placement for elements in focus. + */ + $searchInput.focus(); + if ($searchInput.setSelectionRange) { + $searchInput.setSelectionRange(queryLength, queryLength); + } else { + $searchInput.val(currentSearchVal); + } + } + }, + executeSearch: function(e) { + const $search = $('#issuable_search'); + const $searchName = $search.attr('name'); + const $searchValue = $search.val(); + const $filtersForm = $('.js-filter-form'); + const $input = $(`input[name='${$searchName}']`, $filtersForm); + const isPristine = Issuable.accessSearchPristine(); + + if (isPristine) { + return; + } + + if (!$input.length) { + $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); + } else { + $input.val($searchValue); + } + + Issuable.filterResults($filtersForm); + }, + initLabelFilterRemove: function() { + return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { + var $button; + $button = $(this); + // Remove the label input box + $('input[name="label_name[]"]').filter(function() { + return this.value === $button.data('label'); + }).remove(); + // Submit the form to get new data + Issuable.filterResults($('.filter-form')); + }); + }, + filterResults: (function(_this) { + return function(form) { + var formAction, formData, issuesUrl; + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); + formAction = form.attr('action'); + issuesUrl = formAction; + issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); + issuesUrl += formData; + return gl.utils.visitUrl(issuesUrl); + }; + })(this), + initResetFilters: function() { + $('.reset-filters').on('click', function(e) { + e.preventDefault(); + const target = e.target; + const $form = $(target).parents('.js-filter-form'); + const baseIssuesUrl = target.href; + + $form.attr('action', baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); + }); + }, + initChecks: function() { + this.issuableBulkActions = $('.bulk-update').data('bulkActions'); + $('.check_all_issues').off('click').on('click', function() { + $('.selected_issue').prop('checked', this.checked); + return Issuable.checkChanged(); + }); + return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); + }, + checkChanged: function() { + const $checkedIssues = $('.selected_issue:checked'); + const $updateIssuesIds = $('#update_issuable_ids'); + const $issuesOtherFilters = $('.issues-other-filters'); + const $issuesBulkUpdate = $('.issues_bulk_update'); + + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + + if ($checkedIssues.length > 0) { + const ids = $.map($checkedIssues, function(value) { + return $(value).data('id'); + }); + $updateIssuesIds.val(ids); + $issuesOtherFilters.hide(); + $issuesBulkUpdate.show(); + } else { + $updateIssuesIds.val([]); + $issuesBulkUpdate.hide(); + $issuesOtherFilters.show(); + } + return true; + }, + + resetIncomingEmailToken: function() { + $('.incoming-email-token-reset').on('click', function(e) { + e.preventDefault(); + + $.ajax({ + type: 'PUT', + url: $('.incoming-email-token-reset').attr('href'), + dataType: 'json', + success: function(response) { + $('#issue_email').val(response.new_issue_address).focus(); + }, + beforeSend: function() { + $('.incoming-email-token-reset').text('resetting...'); + }, + complete: function() { + $('.incoming-email-token-reset').text('reset it'); + } + }); + }); + } + }; +})(window); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 deleted file mode 100644 index 8df86f68218..00000000000 --- a/app/assets/javascripts/issuable.js.es6 +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ -/* global Issuable */ - -((global) => { - var issuable_created; - - issuable_created = false; - - global.Issuable = { - init: function() { - Issuable.initTemplates(); - Issuable.initSearch(); - Issuable.initChecks(); - Issuable.initResetFilters(); - Issuable.resetIncomingEmailToken(); - return Issuable.initLabelFilterRemove(); - }, - initTemplates: function() { - return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); - }, - initSearch: function() { - const $searchInput = $('#issuable_search'); - - Issuable.initSearchState($searchInput); - - // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing - const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); - - $searchInput.off('keyup').on('keyup', debouncedExecSearch); - - // ensures existing filters are preserved when manually submitted - $('#issuable_search_form').on('submit', (e) => { - e.preventDefault(); - debouncedExecSearch(e); - }); - }, - initSearchState: function($searchInput) { - const currentSearchVal = $searchInput.val(); - - Issuable.searchState = { - elem: $searchInput, - current: currentSearchVal - }; - - Issuable.maybeFocusOnSearch(); - }, - accessSearchPristine: function(set) { - // store reference to previous value to prevent search on non-mutating keyup - const state = Issuable.searchState; - const currentSearchVal = state.elem.val(); - - if (set) { - state.current = currentSearchVal; - } else { - return state.current === currentSearchVal; - } - }, - maybeFocusOnSearch: function() { - const currentSearchVal = Issuable.searchState.current; - if (currentSearchVal && currentSearchVal !== '') { - const queryLength = currentSearchVal.length; - const $searchInput = Issuable.searchState.elem; - - /* The following ensures that the cursor is initially placed at - * the end of search input when focus is applied. It accounts - * for differences in browser implementations of `setSelectionRange` - * and cursor placement for elements in focus. - */ - $searchInput.focus(); - if ($searchInput.setSelectionRange) { - $searchInput.setSelectionRange(queryLength, queryLength); - } else { - $searchInput.val(currentSearchVal); - } - } - }, - executeSearch: function(e) { - const $search = $('#issuable_search'); - const $searchName = $search.attr('name'); - const $searchValue = $search.val(); - const $filtersForm = $('.js-filter-form'); - const $input = $(`input[name='${$searchName}']`, $filtersForm); - const isPristine = Issuable.accessSearchPristine(); - - if (isPristine) { - return; - } - - if (!$input.length) { - $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); - } else { - $input.val($searchValue); - } - - Issuable.filterResults($filtersForm); - }, - initLabelFilterRemove: function() { - return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { - var $button; - $button = $(this); - // Remove the label input box - $('input[name="label_name[]"]').filter(function() { - return this.value === $button.data('label'); - }).remove(); - // Submit the form to get new data - Issuable.filterResults($('.filter-form')); - }); - }, - filterResults: (function(_this) { - return function(form) { - var formAction, formData, issuesUrl; - formData = form.serializeArray(); - formData = formData.filter(function(data) { - return data.value !== ''; - }); - formData = $.param(formData); - formAction = form.attr('action'); - issuesUrl = formAction; - issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); - issuesUrl += formData; - return gl.utils.visitUrl(issuesUrl); - }; - })(this), - initResetFilters: function() { - $('.reset-filters').on('click', function(e) { - e.preventDefault(); - const target = e.target; - const $form = $(target).parents('.js-filter-form'); - const baseIssuesUrl = target.href; - - $form.attr('action', baseIssuesUrl); - gl.utils.visitUrl(baseIssuesUrl); - }); - }, - initChecks: function() { - this.issuableBulkActions = $('.bulk-update').data('bulkActions'); - $('.check_all_issues').off('click').on('click', function() { - $('.selected_issue').prop('checked', this.checked); - return Issuable.checkChanged(); - }); - return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); - }, - checkChanged: function() { - const $checkedIssues = $('.selected_issue:checked'); - const $updateIssuesIds = $('#update_issuable_ids'); - const $issuesOtherFilters = $('.issues-other-filters'); - const $issuesBulkUpdate = $('.issues_bulk_update'); - - this.issuableBulkActions.willUpdateLabels = false; - this.issuableBulkActions.setOriginalDropdownData(); - - if ($checkedIssues.length > 0) { - const ids = $.map($checkedIssues, function(value) { - return $(value).data('id'); - }); - $updateIssuesIds.val(ids); - $issuesOtherFilters.hide(); - $issuesBulkUpdate.show(); - } else { - $updateIssuesIds.val([]); - $issuesBulkUpdate.hide(); - $issuesOtherFilters.show(); - } - return true; - }, - - resetIncomingEmailToken: function() { - $('.incoming-email-token-reset').on('click', function(e) { - e.preventDefault(); - - $.ajax({ - type: 'PUT', - url: $('.incoming-email-token-reset').attr('href'), - dataType: 'json', - success: function(response) { - $('#issue_email').val(response.new_issue_address).focus(); - }, - beforeSend: function() { - $('.incoming-email-token-reset').text('resetting...'); - }, - complete: function() { - $('.incoming-email-token-reset').text('reset it'); - } - }); - }); - } - }; -})(window); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js index e927cc0077c..e927cc0077c 100644 --- a/app/assets/javascripts/issuable/issuable_bundle.js.es6 +++ b/app/assets/javascripts/issuable/issuable_bundle.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js new file mode 100644 index 00000000000..357b3487ca9 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js @@ -0,0 +1,42 @@ +/* global Vue */ +import stopwatchSvg from 'icons/_icon_stopwatch.svg'; + +require('../../../lib/utils/pretty_time'); + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + <div class='sidebar-collapsed-icon'> + ${stopwatchSvg} + <div class='time-tracking-collapsed-summary'> + <div class='compare' v-if='showComparisonState'> + <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='estimate-only' v-if='showEstimateOnlyState'> + <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='spend-only' v-if='showSpentOnlyState'> + <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span> + </div> + <div class='no-tracking' v-if='showNoTimeTrackingState'> + <span class='no-value'>None</span> + </div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 deleted file mode 100644 index bf27fbac5d7..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ /dev/null @@ -1,41 +0,0 @@ -/* global Vue */ -require('../../../lib/utils/pretty_time'); - -(() => { - Vue.component('time-tracking-collapsed-state', { - name: 'time-tracking-collapsed-state', - props: [ - 'showComparisonState', - 'showSpentOnlyState', - 'showEstimateOnlyState', - 'showNoTimeTrackingState', - 'timeSpentHumanReadable', - 'timeEstimateHumanReadable', - 'stopwatchSvg', - ], - methods: { - abbreviateTime(timeStr) { - return gl.utils.prettyTime.abbreviateTime(timeStr); - }, - }, - template: ` - <div class='sidebar-collapsed-icon'> - <div v-html='stopwatchSvg'></div> - <div class='time-tracking-collapsed-summary'> - <div class='compare' v-if='showComparisonState'> - <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> - </div> - <div class='estimate-only' v-if='showEstimateOnlyState'> - <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> - </div> - <div class='spend-only' v-if='showSpentOnlyState'> - <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span> - </div> - <div class='no-tracking' v-if='showNoTimeTrackingState'> - <span class='no-value'>None</span> - </div> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js index 750468c679b..750468c679b 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js index 309e9f2f9ef..309e9f2f9ef 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js index d7ced6d7151..d7ced6d7151 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js index 1d2ca643b5b..1d2ca643b5b 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js index ed283fec3c3..ed283fec3c3 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js new file mode 100644 index 00000000000..1fae2d62b14 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js @@ -0,0 +1,117 @@ +/* global Vue */ + +require('./help_state'); +require('./collapsed_state'); +require('./spent_only_pane'); +require('./no_tracking_pane'); +require('./estimate_only_pane'); +require('./comparison_pane'); + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` + <div class='time_tracker time-tracking-component-wrap' v-cloak> + <time-tracking-collapsed-state + :show-comparison-state='showComparisonState' + :show-help-state='showHelpState' + :show-spent-only-state='showSpentOnlyState' + :show-estimate-only-state='showEstimateOnlyState' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-collapsed-state> + <div class='title hide-collapsed'> + Time tracking + <div class='help-button pull-right' + v-if='!showHelpState' + @click='toggleHelpState(true)'> + <i class='fa fa-question-circle' aria-hidden='true'></i> + </div> + <div class='close-help-button pull-right' + v-if='showHelpState' + @click='toggleHelpState(false)'> + <i class='fa fa-close' aria-hidden='true'></i> + </div> + </div> + <div class='time-tracking-content hide-collapsed'> + <time-tracking-estimate-only-pane + v-if='showEstimateOnlyState' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-estimate-only-pane> + <time-tracking-spent-only-pane + v-if='showSpentOnlyState' + :time-spent-human-readable='timeSpentHumanReadable'> + </time-tracking-spent-only-pane> + <time-tracking-no-tracking-pane + v-if='showNoTimeTrackingState'> + </time-tracking-no-tracking-pane> + <time-tracking-comparison-pane + v-if='showComparisonState' + :time-estimate='timeEstimate' + :time-spent='timeSpent' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-comparison-pane> + <transition name='help-state-toggle'> + <time-tracking-help-state + v-if='showHelpState' + :docs-url='docsUrl'> + </time-tracking-help-state> + </transition> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 deleted file mode 100644 index e38f7852b1c..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 +++ /dev/null @@ -1,119 +0,0 @@ -/* global Vue */ - -require('./help_state'); -require('./collapsed_state'); -require('./spent_only_pane'); -require('./no_tracking_pane'); -require('./estimate_only_pane'); -require('./comparison_pane'); - -(() => { - Vue.component('issuable-time-tracker', { - name: 'issuable-time-tracker', - props: [ - 'time_estimate', - 'time_spent', - 'human_time_estimate', - 'human_time_spent', - 'stopwatchSvg', - 'docsUrl', - ], - data() { - return { - showHelp: false, - }; - }, - computed: { - timeSpent() { - return this.time_spent; - }, - timeEstimate() { - return this.time_estimate; - }, - timeEstimateHumanReadable() { - return this.human_time_estimate; - }, - timeSpentHumanReadable() { - return this.human_time_spent; - }, - hasTimeSpent() { - return !!this.timeSpent; - }, - hasTimeEstimate() { - return !!this.timeEstimate; - }, - showComparisonState() { - return this.hasTimeEstimate && this.hasTimeSpent; - }, - showEstimateOnlyState() { - return this.hasTimeEstimate && !this.hasTimeSpent; - }, - showSpentOnlyState() { - return this.hasTimeSpent && !this.hasTimeEstimate; - }, - showNoTimeTrackingState() { - return !this.hasTimeEstimate && !this.hasTimeSpent; - }, - showHelpState() { - return !!this.showHelp; - }, - }, - methods: { - toggleHelpState(show) { - this.showHelp = show; - }, - }, - template: ` - <div class='time_tracker time-tracking-component-wrap' v-cloak> - <time-tracking-collapsed-state - :show-comparison-state='showComparisonState' - :show-help-state='showHelpState' - :show-spent-only-state='showSpentOnlyState' - :show-estimate-only-state='showEstimateOnlyState' - :time-spent-human-readable='timeSpentHumanReadable' - :time-estimate-human-readable='timeEstimateHumanReadable' - :stopwatch-svg='stopwatchSvg'> - </time-tracking-collapsed-state> - <div class='title hide-collapsed'> - Time tracking - <div class='help-button pull-right' - v-if='!showHelpState' - @click='toggleHelpState(true)'> - <i class='fa fa-question-circle'></i> - </div> - <div class='close-help-button pull-right' - v-if='showHelpState' - @click='toggleHelpState(false)'> - <i class='fa fa-close'></i> - </div> - </div> - <div class='time-tracking-content hide-collapsed'> - <time-tracking-estimate-only-pane - v-if='showEstimateOnlyState' - :time-estimate-human-readable='timeEstimateHumanReadable'> - </time-tracking-estimate-only-pane> - <time-tracking-spent-only-pane - v-if='showSpentOnlyState' - :time-spent-human-readable='timeSpentHumanReadable'> - </time-tracking-spent-only-pane> - <time-tracking-no-tracking-pane - v-if='showNoTimeTrackingState'> - </time-tracking-no-tracking-pane> - <time-tracking-comparison-pane - v-if='showComparisonState' - :time-estimate='timeEstimate' - :time-spent='timeSpent' - :time-spent-human-readable='timeSpentHumanReadable' - :time-estimate-human-readable='timeEstimateHumanReadable'> - </time-tracking-comparison-pane> - <transition name='help-state-toggle'> - <time-tracking-help-state - v-if='showHelpState' - :docs-url='docsUrl'> - </time-tracking-help-state> - </transition> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js new file mode 100644 index 00000000000..0134b7cb6f3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js @@ -0,0 +1,65 @@ +/* global Vue */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('./components/time_tracker'); +require('../../smart_interval'); +require('../../subbable_resource'); + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes + ? Object.keys(data.commands_changes) + : []; + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 deleted file mode 100644 index 1ca01d3bdb9..00000000000 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ /dev/null @@ -1,62 +0,0 @@ -/* global Vue */ - -require('./components/time_tracker'); -require('../../smart_interval'); -require('../../subbable_resource'); - -(() => { - /* This Vue instance represents what will become the parent instance for the - * sidebar. It will be responsible for managing `issuable` state and propagating - * changes to sidebar components. We will want to create a separate service to - * interface with the server at that point. - */ - - class IssuableTimeTracking { - constructor(issuableJSON) { - const parsedIssuable = JSON.parse(issuableJSON); - return this.initComponent(parsedIssuable); - } - - initComponent(parsedIssuable) { - this.parentInstance = new Vue({ - el: '#issuable-time-tracker', - data: { - issuable: parsedIssuable, - }, - methods: { - fetchIssuable() { - return gl.IssuableResource.get.call(gl.IssuableResource, { - type: 'GET', - url: gl.IssuableResource.endpoint, - }); - }, - updateState(data) { - this.issuable = data; - }, - subscribeToUpdates() { - gl.IssuableResource.subscribe(data => this.updateState(data)); - }, - listenForSlashCommands() { - $(document).on('ajax:success', '.gfm-form', (e, data) => { - const subscribedCommands = ['spend_time', 'time_estimate']; - const changedCommands = data.commands_changes; - - if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { - this.fetchIssuable(); - } - }); - }, - }, - created() { - this.fetchIssuable(); - }, - mounted() { - this.subscribeToUpdates(); - this.listenForSlashCommands(); - }, - }); - } - } - - gl.IssuableTimeTracking = IssuableTimeTracking; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 52457f70d90..47e675f537e 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -2,134 +2,130 @@ /* global Flash */ require('./flash'); +require('~/lib/utils/text_utility'); require('vendor/jquery.waitforimages'); require('./task_list'); -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - - this.Issue = (function() { - function Issue() { - this.submitNoteForm = bind(this.submitNoteForm, this); - if ($('a.btn-close').length) { - this.taskList = new gl.TaskList({ - dataType: 'issue', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: (result) => { - document.querySelector('#task_status').innerText = result.task_status; - document.querySelector('#task_status_short').innerText = result.task_status_short; - } - }); - this.initIssueBtnEventListeners(); - } - this.initMergeRequests(); - this.initRelatedBranches(); - this.initCanCreateBranch(); +class Issue { + constructor() { + if ($('a.btn-close').length) { + this.taskList = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); + Issue.initIssueBtnEventListeners(); } + Issue.initMergeRequests(); + Issue.initRelatedBranches(); + Issue.initCanCreateBranch(); + } - Issue.prototype.initIssueBtnEventListeners = function() { - var _this, issueFailMessage; - _this = this; - issueFailMessage = 'Unable to update this issue at this time.'; - return $('a.btn-close, a.btn-reopen').on('click', function(e) { - var $this, isClose, shouldSubmit, url; - e.preventDefault(); - e.stopImmediatePropagation(); - $this = $(this); - isClose = $this.hasClass('btn-close'); - shouldSubmit = $this.hasClass('btn-comment'); - if (shouldSubmit) { - _this.submitNoteForm($this.closest('form')); - } - $this.prop('disabled', true); - url = $this.attr('href'); - return $.ajax({ - type: 'PUT', - url: url, - error: function(jqXHR, textStatus, errorThrown) { - var issueStatus; - issueStatus = isClose ? 'close' : 'open'; - return new Flash(issueFailMessage, 'alert'); - }, - success: function(data, textStatus, jqXHR) { - if ('id' in data) { - $(document).trigger('issuable:change'); - const currentTotal = Number($('.issue_counter').text()); - if (isClose) { - $('a.btn-close').addClass('hidden'); - $('a.btn-reopen').removeClass('hidden'); - $('div.status-box-closed').removeClass('hidden'); - $('div.status-box-open').addClass('hidden'); - $('.issue_counter').text(currentTotal - 1); - } else { - $('a.btn-reopen').addClass('hidden'); - $('a.btn-close').removeClass('hidden'); - $('div.status-box-closed').addClass('hidden'); - $('div.status-box-open').removeClass('hidden'); - $('.issue_counter').text(currentTotal + 1); - } + static initIssueBtnEventListeners() { + var issueFailMessage; + issueFailMessage = 'Unable to update this issue at this time.'; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, isClose, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(this); + isClose = $this.hasClass('btn-close'); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit) { + Issue.submitNoteForm($this.closest('form')); + } + $this.prop('disabled', true); + url = $this.attr('href'); + return $.ajax({ + type: 'PUT', + url: url, + error: function(jqXHR, textStatus, errorThrown) { + var issueStatus; + issueStatus = isClose ? 'close' : 'open'; + return new Flash(issueFailMessage, 'alert'); + }, + success: function(data, textStatus, jqXHR) { + if ('id' in data) { + $(document).trigger('issuable:change'); + let total = Number($('.issue_counter').text().replace(/[^\d]/, '')); + if (isClose) { + $('a.btn-close').addClass('hidden'); + $('a.btn-reopen').removeClass('hidden'); + $('div.status-box-closed').removeClass('hidden'); + $('div.status-box-open').addClass('hidden'); + total -= 1; } else { - new Flash(issueFailMessage, 'alert'); + $('a.btn-reopen').addClass('hidden'); + $('a.btn-close').removeClass('hidden'); + $('div.status-box-closed').addClass('hidden'); + $('div.status-box-open').removeClass('hidden'); + total += 1; } - return $this.prop('disabled', false); + $('.issue_counter').text(gl.text.addDelimiter(total)); + } else { + new Flash(issueFailMessage, 'alert'); } - }); + return $this.prop('disabled', false); + } }); - }; + }); + } - Issue.prototype.submitNoteForm = function(form) { - var noteText; - noteText = form.find("textarea.js-note-text").val(); - if (noteText.trim().length > 0) { - return form.submit(); - } - }; + static submitNoteForm(form) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + return form.submit(); + } + } - Issue.prototype.initMergeRequests = function() { - var $container; - $container = $('#merge-requests'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load referenced merge requests', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initMergeRequests() { + var $container; + $container = $('#merge-requests'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load referenced merge requests', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initRelatedBranches = function() { - var $container; - $container = $('#related-branches'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load related branches', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initRelatedBranches() { + var $container; + $container = $('#related-branches'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related branches', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initCanCreateBranch = function() { - var $container; - $container = $('#new-branch'); - // If the user doesn't have the required permissions the container isn't - // rendered at all. - if ($container.length === 0) { - return; + static initCanCreateBranch() { + var $container; + $container = $('#new-branch'); + // If the user doesn't have the required permissions the container isn't + // rendered at all. + if ($container.length === 0) { + return; + } + return $.getJSON($container.data('path')).error(function() { + $container.find('.unavailable').show(); + return new Flash('Failed to check if a new branch can be created.', 'alert'); + }).success(function(data) { + if (data.can_create_branch) { + $container.find('.available').show(); + } else { + return $container.find('.unavailable').show(); } - return $.getJSON($container.data('path')).error(function() { - $container.find('.unavailable').show(); - return new Flash('Failed to check if a new branch can be created.', 'alert'); - }).success(function(data) { - if (data.can_create_branch) { - $container.find('.available').show(); - } else { - return $container.find('.unavailable').show(); - } - }); - }; + }); + } +} - return Issue; - })(); -}).call(window); +export default Issue; diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js index e0ebd36a65c..e0ebd36a65c 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js index 38b2eb9ff14..38b2eb9ff14 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 9e2d14c7f87..443fb3e0ca9 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -76,7 +76,7 @@ if (!selected.length) { data[abilityName].label_ids = ['']; } - $loading.fadeIn(); + $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); return $.ajax({ type: 'PUT', @@ -353,31 +353,17 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsModel = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsModel = gl.issueBoards.ModalStore.store.filter; } if (boardsModel) { if (label.isAny) { boardsModel['label_name'] = []; - } - else if ($el.hasClass('is-active')) { + } else if ($el.hasClass('is-active')) { boardsModel['label_name'].push(label.title); } - else { - var filters = boardsModel['label_name']; - filters = filters.filter(function (filteredLabel) { - return filteredLabel !== label.title; - }); - boardsModel['label_name'] = filters; - } - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); return; } diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js deleted file mode 100644 index 9b011d89e93..00000000000 --- a/app/assets/javascripts/lib/chart.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -window.Chart = require('vendor/Chart'); diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js deleted file mode 100644 index 7862c6797c3..00000000000 --- a/app/assets/javascripts/lib/cropper.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require cropper */ - -(function() { - -}).call(window); diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js deleted file mode 100644 index a9dd32edbed..00000000000 --- a/app/assets/javascripts/lib/d3.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -window.d3 = require('d3'); diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js deleted file mode 100644 index ebe1e2ae98d..00000000000 --- a/app/assets/javascripts/lib/raphael.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require raphael */ -/*= require g.raphael */ -/*= require g.bar */ - -(function() { - -}).call(window); diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index 2955bda1a36..2955bda1a36 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js new file mode 100644 index 00000000000..a1423b6fda5 --- /dev/null +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -0,0 +1,342 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ +(function() { + (function(w) { + var base; + w.gl || (w.gl = {}); + (base = w.gl).utils || (base.utils = {}); + w.gl.utils.isInGroupsPage = function() { + return gl.utils.getPagePath() === 'groups'; + }; + w.gl.utils.isInProjectPage = function() { + return gl.utils.getPagePath() === 'projects'; + }; + w.gl.utils.getProjectSlug = function() { + if (this.isInProjectPage()) { + return $('body').data('project'); + } else { + return null; + } + }; + w.gl.utils.getGroupSlug = function() { + if (this.isInGroupsPage()) { + return $('body').data('group'); + } else { + return null; + } + }; + + w.gl.utils.ajaxGet = function(url) { + return $.ajax({ + type: "GET", + url: url, + dataType: "script" + }); + }; + + w.gl.utils.extractLast = function(term) { + return this.split(term).pop(); + }; + + w.gl.utils.rstrip = function rstrip(val) { + if (val) { + return val.replace(/\s+$/, ''); + } else { + return val; + } + }; + + w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { + event_name = event_name || 'input'; + var closest_submit, field, that; + that = this; + field = $(field_selector); + closest_submit = field.closest('form').find(button_selector); + if (this.rstrip(field.val()) === "") { + closest_submit.disable(); + } + return field.on(event_name, function() { + if (that.rstrip($(this).val()) === "") { + return closest_submit.disable(); + } else { + return closest_submit.enable(); + } + }); + }; + + // automatically adjust scroll position for hash urls taking the height of the navbar into account + // https://github.com/twitter/bootstrap/issues/1768 + w.gl.utils.handleLocationHash = function() { + var hash = w.gl.utils.getLocationHash(); + if (!hash) return; + + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + var target = document.getElementById('user-content-' + hash); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + var fixedTabs = document.querySelector('.js-tabs-affix'); + if (fixedTabs) { + window.scrollBy(0, -fixedTabs.offsetHeight); + } + } + }; + + // Check if element scrolled into viewport from above or below + // Courtesy http://stackoverflow.com/a/7557433/414749 + w.gl.utils.isInViewport = function(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); + }; + + gl.utils.getPagePath = function(index) { + index = index || 0; + return $('body').data('page').split(':')[index]; + }; + + gl.utils.parseUrl = function (url) { + var parser = document.createElement('a'); + parser.href = url; + return parser; + }; + + gl.utils.parseUrlPathname = function (url) { + var parsedUrl = gl.utils.parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; + }; + + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + }; + + gl.utils.isMetaKey = function(e) { + return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + }; + + gl.utils.isMetaClick = function(e) { + // Identify following special clicks + // 1) Cmd + Click on Mac (e.metaKey) + // 2) Ctrl + Click on PC (e.ctrlKey) + // 3) Middle-click or Mouse Wheel Click (e.which is 2) + return e.metaKey || e.ctrlKey || e.which === 2; + }; + + gl.utils.scrollToElement = function($el) { + var top = $el.offset().top; + gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); + + return $('body, html').animate({ + scrollTop: top - (gl.mrTabsHeight) + }, 200); + }; + + /** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided + */ + w.gl.utils.getParameterByName = (name) => { + const url = window.location.href; + name = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + + w.gl.utils.getSelectedFragment = () => { + const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; + const documentFragment = selection.getRangeAt(0).cloneContents(); + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; + }; + + w.gl.utils.insertText = (target, text) => { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas + + const selectionStart = target.selectionStart; + const selectionEnd = target.selectionEnd; + const value = target.value; + + const textBefore = value.substring(0, selectionStart); + const textAfter = value.substring(selectionEnd, value.length); + const newText = textBefore + text + textAfter; + + target.value = newText; + target.selectionStart = target.selectionEnd = selectionStart + text.length; + + // Trigger autosave + $(target).trigger('input'); + + // Trigger autosize + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + target.dispatchEvent(event); + }; + + w.gl.utils.nodeMatchesSelector = (node, selector) => { + const matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + const matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; + }; + + /** + this will take in the headers from an API response and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys + */ + w.gl.utils.normalizeHeaders = (headers) => { + const upperCaseHeaders = {}; + + Object.keys(headers).forEach((e) => { + upperCaseHeaders[e.toUpperCase()] = headers[e]; + }); + + return upperCaseHeaders; + }; + + /** + * Parses pagination object string values into numbers. + * + * @param {Object} paginationInformation + * @returns {Object} + */ + w.gl.utils.parseIntPagination = paginationInformation => ({ + perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), + page: parseInt(paginationInformation['X-PAGE'], 10), + total: parseInt(paginationInformation['X-TOTAL'], 10), + totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), + nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), + previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), + }); + + /** + * Updates the search parameter of a URL given the parameter and values provided. + * + * If no search params are present we'll add it. + * If param for page is already present, we'll update it + * If there are params but not for the given one, we'll add it at the end. + * Returns the new search parameters. + * + * @param {String} param + * @param {Number|String|Undefined|Null} value + * @return {String} + */ + w.gl.utils.setParamInURL = (param, value) => { + let search; + const locationSearch = window.location.search; + + if (locationSearch.length === 0) { + search = `?${param}=${value}`; + } + + if (locationSearch.indexOf(param) !== -1) { + const regex = new RegExp(param + '=\\d'); + search = locationSearch.replace(regex, `${param}=${value}`); + } + + if (locationSearch.length && locationSearch.indexOf(param) === -1) { + search = `${locationSearch}&${param}=${value}`; + } + + return search; + }; + + /** + * Converts permission provided as strings to booleans. + * + * @param {String} string + * @returns {Boolean} + */ + w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; + + /** + * Back Off exponential algorithm + * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> + * + * @param {Function<next, stop>} fn function to be called + * @param {Number} timeout + * @return {Promise<Any, Error>} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ + w.gl.utils.backOff = (fn, timeout = 60000) => { + const maxInterval = 32000; + let nextInterval = 2000; + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (Date.now() - startTime < timeout) { + setTimeout(fn.bind(null, next, stop), nextInterval); + nextInterval = Math.min(nextInterval + nextInterval, maxInterval); + } else { + reject(new Error('BACKOFF_TIMEOUT')); + } + }; + + fn(next, stop); + }); + }; + })(window); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 deleted file mode 100644 index 45a1d90a9d9..00000000000 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ /dev/null @@ -1,300 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ -(function() { - (function(w) { - var base; - w.gl || (w.gl = {}); - (base = w.gl).utils || (base.utils = {}); - w.gl.utils.isInGroupsPage = function() { - return gl.utils.getPagePath() === 'groups'; - }; - w.gl.utils.isInProjectPage = function() { - return gl.utils.getPagePath() === 'projects'; - }; - w.gl.utils.getProjectSlug = function() { - if (this.isInProjectPage()) { - return $('body').data('project'); - } else { - return null; - } - }; - w.gl.utils.getGroupSlug = function() { - if (this.isInGroupsPage()) { - return $('body').data('group'); - } else { - return null; - } - }; - - w.gl.utils.ajaxGet = function(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "script" - }); - }; - - w.gl.utils.extractLast = function(term) { - return this.split(term).pop(); - }; - - w.gl.utils.rstrip = function rstrip(val) { - if (val) { - return val.replace(/\s+$/, ''); - } else { - return val; - } - }; - - w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { - event_name = event_name || 'input'; - var closest_submit, field, that; - that = this; - field = $(field_selector); - closest_submit = field.closest('form').find(button_selector); - if (this.rstrip(field.val()) === "") { - closest_submit.disable(); - } - return field.on(event_name, function() { - if (that.rstrip($(this).val()) === "") { - return closest_submit.disable(); - } else { - return closest_submit.enable(); - } - }); - }; - - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - w.gl.utils.handleLocationHash = function() { - var hash = w.gl.utils.getLocationHash(); - if (!hash) return; - - // This is required to handle non-unicode characters in hash - hash = decodeURIComponent(hash); - - // scroll to user-generated markdown anchor if we cannot find a match - if (document.getElementById(hash) === null) { - var target = document.getElementById('user-content-' + hash); - if (target && target.scrollIntoView) { - target.scrollIntoView(true); - } - } else { - // only adjust for fixedTabs when not targeting user-generated content - var fixedTabs = document.querySelector('.js-tabs-affix'); - if (fixedTabs) { - window.scrollBy(0, -fixedTabs.offsetHeight); - } - } - }; - - // Check if element scrolled into viewport from above or below - // Courtesy http://stackoverflow.com/a/7557433/414749 - w.gl.utils.isInViewport = function(el) { - var rect = el.getBoundingClientRect(); - - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth - ); - }; - - gl.utils.getPagePath = function(index) { - index = index || 0; - return $('body').data('page').split(':')[index]; - }; - - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - - gl.utils.parseUrlPathname = function (url) { - var parsedUrl = gl.utils.parseUrl(url); - // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 - // We have to make sure we always have an absolute path. - return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; - }; - - gl.utils.getUrlParamsArray = function () { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&'); - }; - - gl.utils.isMetaKey = function(e) { - return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; - }; - - gl.utils.isMetaClick = function(e) { - // Identify following special clicks - // 1) Cmd + Click on Mac (e.metaKey) - // 2) Ctrl + Click on PC (e.ctrlKey) - // 3) Middle-click or Mouse Wheel Click (e.which is 2) - return e.metaKey || e.ctrlKey || e.which === 2; - }; - - gl.utils.scrollToElement = function($el) { - var top = $el.offset().top; - gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); - - return $('body, html').animate({ - scrollTop: top - (gl.mrTabsHeight) - }, 200); - }; - - /** - this will take in the `name` of the param you want to parse in the url - if the name does not exist this function will return `null` - otherwise it will return the value of the param key provided - */ - w.gl.utils.getParameterByName = (name) => { - const url = window.location.href; - name = name.replace(/[[\]]/g, '\\$&'); - const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); - const results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - - w.gl.utils.getSelectedFragment = () => { - const selection = window.getSelection(); - if (selection.rangeCount === 0) return null; - const documentFragment = selection.getRangeAt(0).cloneContents(); - if (documentFragment.textContent.length === 0) return null; - - return documentFragment; - }; - - w.gl.utils.insertText = (target, text) => { - // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas - - const selectionStart = target.selectionStart; - const selectionEnd = target.selectionEnd; - const value = target.value; - - const textBefore = value.substring(0, selectionStart); - const textAfter = value.substring(selectionEnd, value.length); - const newText = textBefore + text + textAfter; - - target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + text.length; - - // Trigger autosave - $(target).trigger('input'); - - // Trigger autosize - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - target.dispatchEvent(event); - }; - - w.gl.utils.nodeMatchesSelector = (node, selector) => { - const matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector; - - if (matches) { - return matches.call(node, selector); - } - - // IE11 doesn't support `node.matches(selector)` - - let parentNode = node.parentNode; - if (!parentNode) { - parentNode = document.createElement('div'); - node = node.cloneNode(true); - parentNode.appendChild(node); - } - - const matchingNodes = parentNode.querySelectorAll(selector); - return Array.prototype.indexOf.call(matchingNodes, node) !== -1; - }; - - /** - this will take in the headers from an API response and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeHeaders = (headers) => { - const upperCaseHeaders = {}; - - Object.keys(headers).forEach((e) => { - upperCaseHeaders[e.toUpperCase()] = headers[e]; - }); - - return upperCaseHeaders; - }; - - /** - * Parses pagination object string values into numbers. - * - * @param {Object} paginationInformation - * @returns {Object} - */ - w.gl.utils.parseIntPagination = paginationInformation => ({ - perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), - page: parseInt(paginationInformation['X-PAGE'], 10), - total: parseInt(paginationInformation['X-TOTAL'], 10), - totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), - nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), - previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), - }); - - /** - * Transforms a DOMStringMap into a plain object. - * - * @param {DOMStringMap} DOMStringMapObject - * @returns {Object} - */ - w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { - acc[element] = DOMStringMapObject[element]; - return acc; - }, {}); - - /** - * Updates the search parameter of a URL given the parameter and values provided. - * - * If no search params are present we'll add it. - * If param for page is already present, we'll update it - * If there are params but not for the given one, we'll add it at the end. - * Returns the new search parameters. - * - * @param {String} param - * @param {Number|String|Undefined|Null} value - * @return {String} - */ - w.gl.utils.setParamInURL = (param, value) => { - let search; - const locationSearch = window.location.search; - - if (locationSearch.length === 0) { - search = `?${param}=${value}`; - } - - if (locationSearch.indexOf(param) !== -1) { - const regex = new RegExp(param + '=\\d'); - search = locationSearch.replace(regex, `${param}=${value}`); - } - - if (locationSearch.length && locationSearch.indexOf(param) === -1) { - search = `${locationSearch}&${param}=${value}`; - } - - return search; - }; - - /** - * Converts permission provided as strings to booleans. - * - * @param {String} string - * @returns {Boolean} - */ - w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; - })(window); -}).call(window); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js index 82dcbdc26c8..82dcbdc26c8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 +++ b/app/assets/javascripts/lib/utils/datetime_utility.js diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js new file mode 100644 index 00000000000..bc109a69c20 --- /dev/null +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -0,0 +1,10 @@ +/** + * exports HTTP status codes + */ + +const statusCodes = { + NO_CONTENT: 204, + OK: 200, +}; + +module.exports = statusCodes; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js index ae397212e55..ae397212e55 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js.es6 +++ b/app/assets/javascripts/lib/utils/pretty_time.js diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 579d322e3fb..2e5f8a09fc1 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -65,9 +65,10 @@ require('vendor/latinise'); } }; gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine; + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; removedLastNewLine = false; removedFirstNewLine = false; + currentLineEmpty = false; // Remove the first newline if (selected.indexOf('\n') === 0) { @@ -82,7 +83,17 @@ require('vendor/latinise'); } selectedSplit = selected.split('\n'); - startChar = !wrap && textArea.selectionStart > 0 ? '\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))) { if (blockTag != null) { @@ -142,9 +153,8 @@ require('vendor/latinise'); } }; gl.text.updateText = function(textArea, tag, blockTag, wrap) { - var $textArea, oldVal, selected, text; + var $textArea, selected, text; $textArea = $(textArea); - oldVal = $textArea.val(); textArea = $textArea.get(0); text = $textArea.val(); selected = this.selectedText(text, textArea); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js new file mode 100644 index 00000000000..09c4261b318 --- /dev/null +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -0,0 +1,93 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + // Returns an array containing the value(s) of the + // of the key passed as an argument + w.gl.utils.getParameterValues = function(sParam) { + var i, sPageURL, sParameterName, sURLVariables, values; + sPageURL = decodeURIComponent(window.location.search.substring(1)); + sURLVariables = sPageURL.split('&'); + sParameterName = void 0; + values = []; + i = 0; + while (i < sURLVariables.length) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + values.push(sParameterName[1].replace(/\+/g, ' ')); + } + i += 1; + } + return values; + }; + // @param {Object} params - url keys and value to merge + // @param {String} url + w.gl.utils.mergeUrlParams = function(params, url) { + var lastChar, newUrl, paramName, paramValue, pattern; + newUrl = decodeURIComponent(url); + for (paramName in params) { + paramValue = params[paramName]; + pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); + if (paramValue == null) { + newUrl = newUrl.replace(pattern, ''); + } else if (url.search(pattern) !== -1) { + newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); + } else { + newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; + } + } + // Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { + newUrl = newUrl.slice(0, -1); + } + return newUrl; + }; + // removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = function(url, param) { + var urlVariables, variables; + url = decodeURIComponent(url); + urlVariables = url.split('&'); + return ((function() { + var j, len, results; + results = []; + for (j = 0, len = urlVariables.length; j < len; j += 1) { + variables = urlVariables[j]; + if (variables.indexOf(param) === -1) { + results.push(variables); + } + } + return results; + })()).join('&'); + }; + w.gl.utils.removeParams = (params) => { + const url = new URL(window.location.href); + params.forEach((param) => { + url.search = w.gl.utils.removeParamQueryString(url.search, param); + }); + return url.href; + }; + w.gl.utils.getLocationHash = function(url) { + var hashIndex; + if (typeof url === 'undefined') { + // Note: We can't use window.location.hash here because it's + // not consistent across browsers - Firefox will pre-decode it + url = window.location.href; + } + hashIndex = url.indexOf('#'); + return hashIndex === -1 ? null : url.substring(hashIndex + 1); + }; + + w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); + + w.gl.utils.visitUrl = (url) => { + document.location.href = url; + }; + })(window); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6 deleted file mode 100644 index 1bc81d2e4a4..00000000000 --- a/app/assets/javascripts/lib/utils/url_utility.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ -(function() { - (function(w) { - var base; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).utils == null) { - base.utils = {}; - } - // Returns an array containing the value(s) of the - // of the key passed as an argument - w.gl.utils.getParameterValues = function(sParam) { - var i, sPageURL, sParameterName, sURLVariables, values; - sPageURL = decodeURIComponent(window.location.search.substring(1)); - sURLVariables = sPageURL.split('&'); - sParameterName = void 0; - values = []; - i = 0; - while (i < sURLVariables.length) { - sParameterName = sURLVariables[i].split('='); - if (sParameterName[0] === sParam) { - values.push(sParameterName[1].replace(/\+/g, ' ')); - } - i += 1; - } - return values; - }; - // @param {Object} params - url keys and value to merge - // @param {String} url - w.gl.utils.mergeUrlParams = function(params, url) { - var lastChar, newUrl, paramName, paramValue, pattern; - newUrl = decodeURIComponent(url); - for (paramName in params) { - paramValue = params[paramName]; - pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); - if (paramValue == null) { - newUrl = newUrl.replace(pattern, ''); - } else if (url.search(pattern) !== -1) { - newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); - } else { - newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; - } - } - // Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1]; - if (lastChar === '&') { - newUrl = newUrl.slice(0, -1); - } - return newUrl; - }; - // removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = function(url, param) { - var urlVariables, variables; - url = decodeURIComponent(url); - urlVariables = url.split('&'); - return ((function() { - var j, len, results; - results = []; - for (j = 0, len = urlVariables.length; j < len; j += 1) { - variables = urlVariables[j]; - if (variables.indexOf(param) === -1) { - results.push(variables); - } - } - return results; - })()).join('&'); - }; - w.gl.utils.getLocationHash = function(url) { - var hashIndex; - if (typeof url === 'undefined') { - // Note: We can't use window.location.hash here because it's - // not consistent across browsers - Firefox will pre-decode it - url = window.location.href; - } - hashIndex = url.indexOf('#'); - return hashIndex === -1 ? null : url.substring(hashIndex + 1); - }; - - w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); - - w.gl.utils.visitUrl = (url) => { - document.location.href = url; - }; - })(window); -}).call(window); diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 deleted file mode 100644 index 49babdea2e1..00000000000 --- a/app/assets/javascripts/lib/vue_resource.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 966fcd8ec47..1821ca18053 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -67,17 +67,7 @@ require('vendor/jquery.scrollTo'); } LineHighlighter.prototype.bindEvents = function() { - $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler); - // While it may seem odd to bind to the mousedown event and then throw away - // the click event, there is a method to our madness. - // - // If not done this way, the line number anchor will sometimes keep its - // active state even when the event is cancelled, resulting in an ugly border - // around the link and/or a persisted underline text decoration. - $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); + $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler); }; LineHighlighter.prototype.clickHandler = function(event) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js new file mode 100644 index 00000000000..81d5748191d --- /dev/null +++ b/app/assets/javascripts/main.js @@ -0,0 +1,384 @@ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ +/* global bp */ +/* global Cookies */ +/* global Flash */ +/* global ConfirmDangerModal */ +/* global Aside */ + +import jQuery from 'jquery'; +import _ from 'underscore'; +import Cookies from 'js-cookie'; +import Pikaday from 'pikaday'; +import Dropzone from 'dropzone'; +import Sortable from 'vendor/Sortable'; + +// libraries with import side-effects +import 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; +import 'vendor/fuzzaldrin-plus'; + +// extensions +import './extensions/array'; + +// expose common libraries as globals (TODO: remove these) +window.jQuery = jQuery; +window.$ = jQuery; +window._ = _; +window.Cookies = Cookies; +window.Pikaday = Pikaday; +window.Dropzone = Dropzone; +window.Sortable = Sortable; + +// shortcuts +import './shortcuts'; +import './shortcuts_blob'; +import './shortcuts_dashboard_navigation'; +import './shortcuts_navigation'; +import './shortcuts_find_file'; +import './shortcuts_issuable'; +import './shortcuts_network'; + +// behaviors +import './behaviors/autosize'; +import './behaviors/details_behavior'; +import './behaviors/quick_submit'; +import './behaviors/requires_input'; +import './behaviors/toggler_behavior'; +import './behaviors/bind_in_out'; +import { installGlEmojiElement } from './behaviors/gl_emoji'; +installGlEmojiElement(); + +// blob +import './blob/blob_ci_yaml'; +import './blob/blob_dockerfile_selector'; +import './blob/blob_dockerfile_selectors'; +import './blob/blob_file_dropzone'; +import './blob/blob_gitignore_selector'; +import './blob/blob_gitignore_selectors'; +import './blob/blob_license_selector'; +import './blob/blob_license_selectors'; +import './blob/template_selector'; +import './blob/create_branch_dropdown'; +import './blob/target_branch_dropdown'; + +// templates +import './templates/issuable_template_selector'; +import './templates/issuable_template_selectors'; + +// commit +import './commit/file'; +import './commit/image_file'; + +// lib/utils +import './lib/utils/animate'; +import './lib/utils/bootstrap_linked_tabs'; +import './lib/utils/common_utils'; +import './lib/utils/datetime_utility'; +import './lib/utils/notify'; +import './lib/utils/pretty_time'; +import './lib/utils/text_utility'; +import './lib/utils/type_utility'; +import './lib/utils/url_utility'; + +// u2f +import './u2f/authenticate'; +import './u2f/error'; +import './u2f/register'; +import './u2f/util'; + +// droplab +import './droplab/droplab'; +import './droplab/droplab_ajax'; +import './droplab/droplab_ajax_filter'; +import './droplab/droplab_filter'; + +// everything else +import './abuse_reports'; +import './activities'; +import './admin'; +import './ajax_loading_spinner'; +import './api'; +import './aside'; +import './autosave'; +import AwardsHandler from './awards_handler'; +import './breakpoints'; +import './broadcast_message'; +import './build'; +import './build_artifacts'; +import './build_variables'; +import './ci_lint_editor'; +import './commit'; +import './commits'; +import './compare'; +import './compare_autocomplete'; +import './confirm_danger_modal'; +import './copy_as_gfm'; +import './copy_to_clipboard'; +import './create_label'; +import './diff'; +import './dispatcher'; +import './dropzone_input'; +import './due_date_select'; +import './files_comment_button'; +import './flash'; +import './gfm_auto_complete'; +import './gl_dropdown'; +import './gl_field_error'; +import './gl_field_errors'; +import './gl_form'; +import './group_avatar'; +import './group_label_subscription'; +import './groups_select'; +import './header'; +import './importer_status'; +import './issuable'; +import './issuable_context'; +import './issuable_form'; +import './issue'; +import './issue_status_select'; +import './issues_bulk_assignment'; +import './label_manager'; +import './labels'; +import './labels_select'; +import './layout_nav'; +import './line_highlighter'; +import './logo'; +import './member_expiration_date'; +import './members'; +import './merge_request'; +import './merge_request_tabs'; +import './merge_request_widget'; +import './merged_buttons'; +import './milestone'; +import './milestone_select'; +import './mini_pipeline_graph_dropdown'; +import './namespace_select'; +import './new_branch_form'; +import './new_commit_form'; +import './notes'; +import './notifications_dropdown'; +import './notifications_form'; +import './pager'; +import './pipelines'; +import './preview_markdown'; +import './project'; +import './project_avatar'; +import './project_find_file'; +import './project_fork'; +import './project_import'; +import './project_label_subscription'; +import './project_new'; +import './project_select'; +import './project_show'; +import './project_variables'; +import './projects_list'; +import './render_gfm'; +import './render_math'; +import './right_sidebar'; +import './search'; +import './search_autocomplete'; +import './signin_tabs_memoizer'; +import './single_file_diff'; +import './smart_interval'; +import './snippets_list'; +import './star'; +import './subbable_resource'; +import './subscription'; +import './subscription_select'; +import './syntax_highlight'; +import './task_list'; +import './todos'; +import './tree'; +import './user'; +import './user_tabs'; +import './username_validator'; +import './users_select'; +import './version_check_image'; +import './visibility_select'; +import './wikis'; +import './zen_mode'; + +document.addEventListener('beforeunload', function () { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); +}); + +window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + gl.utils.handleLocationHash(); +}, false); + +$(function () { + var $body = $('body'); + var $document = $(document); + var $window = $(window); + var $sidebarGutterToggle = $('.js-sidebar-toggle'); + var $flash = $('.flash-container'); + var bootstrapBreakpoint = bp.getBreakpointSize(); + var fitSidebarForSize; + + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; + + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function() { + var href = this.getAttribute('href'); + if (href.substr(1) === gl.utils.getLocationHash()) { + setTimeout(gl.utils.handleLocationHash, 1); + } + }); + + // prevent default action for disabled buttons + $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + }); + + $('.js-select-on-focus').on('focusin', function () { + return $(this).select().one('mouseup', function (e) { + return e.preventDefault(); + }); + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input + }); + $('.remove-row').bind('ajax:success', function () { + $(this).tooltip('destroy') + .closest('li') + .fadeOut(); + }); + $('.js-remove-tr').bind('ajax:before', function () { + return $(this).hide(); + }); + $('.js-remove-tr').bind('ajax:success', function () { + return $(this).closest('tr').fadeOut(); + }); + $('select.select2').select2({ + width: 'resolve', + // Initialize select2 selects + dropdownAutoWidth: true + }); + $('.js-select2').bind('select2-close', function () { + return setTimeout((function () { + $('.select2-container-active').removeClass('select2-container-active'); + return $(':focus').blur(); + }), 1); + // Close select2 on escape + }); + // Initialize tooltips + $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; + $body.tooltip({ + selector: '.has-tooltip, [data-toggle="tooltip"]', + placement: function (tip, el) { + return $(el).data('placement') || 'bottom'; + } + }); + $('.trigger-submit').on('change', function () { + return $(this).parents('form').submit(); + // Form submitter + }); + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Flash + if ($flash.length > 0) { + $flash.click(function () { + return $(this).fadeOut(); + }); + $flash.show(); + } + // Disable form buttons while a form is submitting + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { + var buttons; + buttons = $('[type="submit"]', this); + switch (e.type) { + case 'ajax:beforeSend': + case 'submit': + return buttons.disable(); + default: + return buttons.enable(); + } + }); + $(document).ajaxError(function (e, xhrObj) { + var ref = xhrObj.status; + if (xhrObj.status === 401) { + return new Flash('You need to be logged in.', 'alert'); + } else if (ref === 404 || ref === 500) { + return new Flash('Something went wrong on our end.', 'alert'); + } + }); + $('.account-box').hover(function () { + // Show/Hide the profile menu when hovering the account box + return $(this).toggleClass('hover'); + }); + $document.on('click', '.diff-content .js-show-suppressed-diff', function () { + var $container; + $container = $(this).parent(); + $container.next('table').show(); + return $container.remove(); + // Commit show suppressed diff + }); + $('.navbar-toggle').on('click', function () { + $('.header-content .title').toggle(); + $('.header-content .header-logo').toggle(); + $('.header-content .navbar-collapse').toggle(); + return $('.navbar-toggle').toggleClass('active'); + }); + // Show/hide comments on diff + $body.on('click', '.js-toggle-diff-comments', function (e) { + var $this = $(this); + var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $this.toggleClass('active'); + if ($this.hasClass('active')) { + notesHolders.show().find('.hide, .content').show(); + } else { + notesHolders.hide().find('.content').hide(); + } + $(document).trigger('toggle.comments'); + return e.preventDefault(); + }); + $document.off('click', '.js-confirm-danger'); + $document.on('click', '.js-confirm-danger', function (e) { + var btn = $(e.target); + var form = btn.closest('form'); + var text = btn.data('confirm-danger-message'); + e.preventDefault(); + return new ConfirmDangerModal(form, text); + }); + $('input[type="search"]').each(function () { + var $this = $(this); + $this.attr('value', $this.val()); + }); + $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { + var $this; + $this = $(this); + return $this.attr('value', $this.val()); + }); + $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { + var $gutterIcon; + if (breakpoint === 'sm' || breakpoint === 'xs') { + $gutterIcon = $sidebarGutterToggle.find('i'); + if ($gutterIcon.hasClass('fa-angle-double-right')) { + return $sidebarGutterToggle.trigger('click'); + } + } + }); + fitSidebarForSize = function () { + var oldBootstrapBreakpoint; + oldBootstrapBreakpoint = bootstrapBreakpoint; + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + $window.off('resize.app').on('resize.app', function () { + return fitSidebarForSize(); + }); + gl.awardsHandler = new AwardsHandler(); + new Aside(); + + gl.utils.initTimeagoTimeout(); +}); diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js index 129d2dc5f0a..129d2dc5f0a 100644 --- a/app/assets/javascripts/member_expiration_date.js.es6 +++ b/app/assets/javascripts/member_expiration_date.js diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js index e3f367a11eb..e3f367a11eb 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index c7e78fed8fe..c7e78fed8fe 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js index 240c8f98932..240c8f98932 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 97753c50b60..97753c50b60 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js index c012b77e0bf..c012b77e0bf 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 74587df22c5..74587df22c5 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 653e52fb6bf..653e52fb6bf 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js index 53e000d7e9e..53e000d7e9e 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js index 0f475f62ee6..0f475f62ee6 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js index 190336dbd20..190336dbd20 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js new file mode 100644 index 00000000000..0e2af3df071 --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js @@ -0,0 +1,296 @@ +/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */ +/* global notify */ +/* global notifyPermissions */ +/* global merge_request_widget */ + +import './smart_interval'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; + +((global) => { + var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; + + const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> + <div class="ci_widget ci-success"> + <%= ci_success_icon %> + <span> + Deployed to + <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment"> + <%- name %> + </a> + <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> + <%- deployed_at %> + </span> + <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer"> + <i class="fa fa-external-link"></i> + View on <%- external_url_formatted %> + </a> + </span> + <span class="stop-env-container js-stop-env-link"> + <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?"> + <i class="fa fa-stop-circle-o"/> + Stop environment + </a> + </span> + </div> + </div>`; + + global.MergeRequestWidget = (function() { + function MergeRequestWidget(opts) { + // Initialize MergeRequestWidget behavior + // + // check_enable - Boolean, whether to check automerge status + // merge_check_url - String, URL to use to check automerge status + // ci_status_url - String, URL to use to check CI status + // + this.opts = opts; + this.$widgetBody = $('.mr-widget-body'); + $('#modal_merge_info').modal({ + show: false + }); + this.clearEventListeners(); + this.addEventListeners(); + this.getCIStatus(false); + this.retrieveSuccessIcon(); + + this.initMiniPipelineGraph(); + + this.ciStatusInterval = new global.SmartInterval({ + callback: this.getCIStatus.bind(this, true), + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + this.ciEnvironmentStatusInterval = new global.SmartInterval({ + callback: this.getCIEnvironmentsStatus.bind(this), + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); + + notifyPermissions(); + } + + MergeRequestWidget.prototype.clearEventListeners = function() { + return $(document).off('DOMContentLoaded'); + }; + + MergeRequestWidget.prototype.addEventListeners = function() { + var allowedPages; + allowedPages = ['show', 'commits', 'pipelines', 'changes']; + $(document).on('DOMContentLoaded', (function(_this) { + return function() { + var page; + page = $('body').data('page').split(':').last(); + if (allowedPages.indexOf(page) === -1) { + return _this.clearEventListeners(); + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.retrieveSuccessIcon = function() { + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + }; + + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { + if (deleteSourceBranch == null) { + deleteSourceBranch = false; + } + return $.ajax({ + type: 'GET', + url: $('.merge-request').data('url'), + success: (function(_this) { + return function(data) { + var callback, urlSuffix; + if (data.state === "merged") { + urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; + return window.location.href = window.location.pathname + urlSuffix; + } else if (data.merge_error) { + return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + } else { + callback = function() { + return merge_request_widget.mergeInProgress(deleteSourceBranch); + }; + return setTimeout(callback, 2000); + } + }; + })(this), + dataType: 'json' + }); + }; + + MergeRequestWidget.prototype.cancelPolling = function () { + this.ciStatusInterval.cancel(); + this.ciEnvironmentStatusInterval.cancel(); + }; + + MergeRequestWidget.prototype.getMergeStatus = function() { + return $.get(this.opts.merge_check_url, (data) => { + var $html = $(data); + this.updateMergeButton(this.status, this.hasCi, $html); + $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); + $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); + }); + }; + + MergeRequestWidget.prototype.ciLabelForStatus = function(status) { + switch (status) { + case 'success': + return 'passed'; + case 'success_with_warnings': + return 'passed with warnings'; + default: + return status; + } + }; + + MergeRequestWidget.prototype.getCIStatus = function(showNotification) { + var _this; + _this = this; + $('.ci-widget-fetching').show(); + return $.getJSON(this.opts.ci_status_url, (function(_this) { + return function(data) { + var message, status, title; + _this.status = data.status; + _this.hasCi = data.has_ci; + _this.updateMergeButton(_this.status, _this.hasCi); + if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); + if (data.status !== _this.opts.ci_status || + data.sha !== _this.opts.ci_sha || + data.pipeline !== _this.opts.ci_pipeline) { + _this.opts.ci_status = data.status; + _this.showCIStatus(data.status); + if (data.coverage) { + _this.showCICoverage(data.coverage); + } + if (data.pipeline) { + _this.opts.ci_pipeline = data.pipeline; + _this.updatePipelineUrls(data.pipeline); + } + if (data.sha) { + _this.opts.ci_sha = data.sha; + _this.updateCommitUrls(data.sha); + } + if (showNotification && data.status) { + status = _this.ciLabelForStatus(data.status); + if (status === "preparing") { + title = _this.opts.ci_title.preparing; + status = status.charAt(0).toUpperCase() + status.slice(1); + message = _this.opts.ci_message.preparing.replace('{{status}}', status); + } else { + title = _this.opts.ci_title.normal; + message = _this.opts.ci_message.normal.replace('{{status}}', status); + } + title = title.replace('{{status}}', status); + message = message.replace('{{sha}}', data.sha); + message = message.replace('{{title}}', data.title); + notify(title, message, _this.opts.gitlab_icon, function() { + this.close(); + }); + } + } + }; + })(this)); + }; + + MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { + $.getJSON(this.opts.ci_environments_status_url, (environments) => { + if (environments && environments.length) this.renderEnvironments(environments); + }); + }; + + MergeRequestWidget.prototype.renderEnvironments = function(environments) { + for (let i = 0; i < environments.length; i += 1) { + const environment = environments[i]; + if ($(`.mr-state-widget #${environment.id}`).length) return; + const $template = $(DEPLOYMENT_TEMPLATE); + if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); + + if (!environment.stop_url) { + $('.js-stop-env-link', $template).remove(); + } + + if (environment.deployed_at && environment.deployed_at_formatted) { + environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; + } else { + $('.js-environment-timeago', $template).remove(); + environment.name += '.'; + } + environment.ci_success_icon = this.$ciSuccessIcon; + const templateString = _.unescape($template[0].outerHTML); + const template = _.template(templateString)(environment); + this.$widgetBody.before(template); + } + }; + + MergeRequestWidget.prototype.showCIStatus = function(state) { + var allowed_states; + if (state == null) { + return; + } + $('.ci_widget').hide(); + $('.ci_widget.ci-' + state).show(); + + this.initMiniPipelineGraph(); + }; + + MergeRequestWidget.prototype.showCICoverage = function(coverage) { + var text = `Coverage ${coverage}%`; + return $('.ci_widget:visible .ci-coverage').text(text); + }; + + MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) { + const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; + let stateClass = 'btn-danger'; + if (!hasCi) { + stateClass = 'btn-create'; + } else if (indexOf.call(allowed_states, state) !== -1) { + switch (state) { + case "failed": + case "canceled": + case "not_found": + stateClass = 'btn-danger'; + break; + case "running": + stateClass = 'btn-info'; + break; + case "success": + case "success_with_warnings": + stateClass = 'btn-create'; + } + } else { + $('.ci_widget.ci-error').show(); + stateClass = 'btn-danger'; + } + + this.setMergeButtonClass(stateClass, $html); + }; + + MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) { + return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class); + }; + + MergeRequestWidget.prototype.updatePipelineUrls = function(id) { + const pipelineUrl = this.opts.pipeline_path; + $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.updateCommitUrls = function(id) { + const commitsUrl = this.opts.commits_path; + $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.initMiniPipelineGraph = function() { + new MiniPipelineGraph({ + container: '.js-pipeline-inline-mr-widget-graph:visible', + }).bindEvents(); + }; + + return MergeRequestWidget; + })(); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 deleted file mode 100644 index 88f08bbaa34..00000000000 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ /dev/null @@ -1,285 +0,0 @@ -/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */ -/* global notify */ -/* global notifyPermissions */ -/* global merge_request_widget */ - -require('./smart_interval'); - -((global) => { - var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; - - const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> - <div class="ci_widget ci-success"> - <%= ci_success_icon %> - <span> - Deployed to - <a href="<%- url %>" target="_blank" class="environment"> - <%- name %> - </a> - <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> - <%- deployed_at %> - </span> - <a class="js-environment-link" href="<%- external_url %>" target="_blank"> - <i class="fa fa-external-link"></i> - View on <%- external_url_formatted %> - </a> - </span> - <span class="stop-env-container js-stop-env-link"> - <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?"> - <i class="fa fa-stop-circle-o"/> - Stop environment - </a> - </span> - </div> - </div>`; - - global.MergeRequestWidget = (function() { - function MergeRequestWidget(opts) { - // Initialize MergeRequestWidget behavior - // - // check_enable - Boolean, whether to check automerge status - // merge_check_url - String, URL to use to check automerge status - // ci_status_url - String, URL to use to check CI status - // - this.opts = opts; - this.$widgetBody = $('.mr-widget-body'); - $('#modal_merge_info').modal({ - show: false - }); - this.clearEventListeners(); - this.addEventListeners(); - this.getCIStatus(false); - this.retrieveSuccessIcon(); - - this.initMiniPipelineGraph(); - - this.ciStatusInterval = new global.SmartInterval({ - callback: this.getCIStatus.bind(this, true), - startingInterval: 10000, - maxInterval: 30000, - hiddenInterval: 120000, - incrementByFactorOf: 5000, - }); - this.ciEnvironmentStatusInterval = new global.SmartInterval({ - callback: this.getCIEnvironmentsStatus.bind(this), - startingInterval: 30000, - maxInterval: 120000, - hiddenInterval: 240000, - incrementByFactorOf: 15000, - immediateExecution: true, - }); - - notifyPermissions(); - } - - MergeRequestWidget.prototype.clearEventListeners = function() { - return $(document).off('DOMContentLoaded'); - }; - - MergeRequestWidget.prototype.addEventListeners = function() { - var allowedPages; - allowedPages = ['show', 'commits', 'pipelines', 'changes']; - $(document).on('DOMContentLoaded', (function(_this) { - return function() { - var page; - page = $('body').data('page').split(':').last(); - if (allowedPages.indexOf(page) < 0) { - return _this.clearEventListeners(); - } - }; - })(this)); - }; - - MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - }; - - MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { - if (deleteSourceBranch == null) { - deleteSourceBranch = false; - } - return $.ajax({ - type: 'GET', - url: $('.merge-request').data('url'), - success: (function(_this) { - return function(data) { - var callback, urlSuffix; - if (data.state === "merged") { - urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; - return window.location.href = window.location.pathname + urlSuffix; - } else if (data.merge_error) { - return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); - } else { - callback = function() { - return merge_request_widget.mergeInProgress(deleteSourceBranch); - }; - return setTimeout(callback, 2000); - } - }; - })(this), - dataType: 'json' - }); - }; - - MergeRequestWidget.prototype.cancelPolling = function () { - this.ciStatusInterval.cancel(); - this.ciEnvironmentStatusInterval.cancel(); - }; - - MergeRequestWidget.prototype.getMergeStatus = function() { - return $.get(this.opts.merge_check_url, function(data) { - var $html = $(data); - $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); - $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); - }); - }; - - MergeRequestWidget.prototype.ciLabelForStatus = function(status) { - switch (status) { - case 'success': - return 'passed'; - case 'success_with_warnings': - return 'passed with warnings'; - default: - return status; - } - }; - - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { - var _this; - _this = this; - $('.ci-widget-fetching').show(); - return $.getJSON(this.opts.ci_status_url, (function(_this) { - return function(data) { - var message, status, title; - if (!data.status) { - return; - } - if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status || - data.sha !== _this.opts.ci_sha || - data.pipeline !== _this.opts.ci_pipeline) { - _this.opts.ci_status = data.status; - _this.showCIStatus(data.status); - if (data.coverage) { - _this.showCICoverage(data.coverage); - } - if (data.pipeline) { - _this.opts.ci_pipeline = data.pipeline; - _this.updatePipelineUrls(data.pipeline); - } - if (data.sha) { - _this.opts.ci_sha = data.sha; - _this.updateCommitUrls(data.sha); - } - if (showNotification) { - status = _this.ciLabelForStatus(data.status); - if (status === "preparing") { - title = _this.opts.ci_title.preparing; - status = status.charAt(0).toUpperCase() + status.slice(1); - message = _this.opts.ci_message.preparing.replace('{{status}}', status); - } else { - title = _this.opts.ci_title.normal; - message = _this.opts.ci_message.normal.replace('{{status}}', status); - } - title = title.replace('{{status}}', status); - message = message.replace('{{sha}}', data.sha); - message = message.replace('{{title}}', data.title); - notify(title, message, _this.opts.gitlab_icon, function() { - this.close(); - }); - } - } - }; - })(this)); - }; - - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { - $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (environments && environments.length) this.renderEnvironments(environments); - }); - }; - - MergeRequestWidget.prototype.renderEnvironments = function(environments) { - for (let i = 0; i < environments.length; i += 1) { - const environment = environments[i]; - if ($(`.mr-state-widget #${environment.id}`).length) return; - const $template = $(DEPLOYMENT_TEMPLATE); - if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); - - if (!environment.stop_url) { - $('.js-stop-env-link', $template).remove(); - } - - if (environment.deployed_at && environment.deployed_at_formatted) { - environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; - } else { - $('.js-environment-timeago', $template).remove(); - environment.name += '.'; - } - environment.ci_success_icon = this.$ciSuccessIcon; - const templateString = _.unescape($template[0].outerHTML); - const template = _.template(templateString)(environment); - this.$widgetBody.before(template); - } - }; - - MergeRequestWidget.prototype.showCIStatus = function(state) { - var allowed_states; - if (state == null) { - return; - } - $('.ci_widget').hide(); - allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; - if (indexOf.call(allowed_states, state) >= 0) { - $('.ci_widget.ci-' + state).show(); - switch (state) { - case "failed": - case "canceled": - case "not_found": - this.setMergeButtonClass('btn-danger'); - break; - case "running": - this.setMergeButtonClass('btn-info'); - break; - case "success": - case "success_with_warnings": - this.setMergeButtonClass('btn-create'); - } - } else { - $('.ci_widget.ci-error').show(); - this.setMergeButtonClass('btn-danger'); - } - }; - - MergeRequestWidget.prototype.showCICoverage = function(coverage) { - var text; - text = 'Coverage ' + coverage + '%'; - return $('.ci_widget:visible .ci-coverage').text(text); - }; - - MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) { - return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); - }; - - MergeRequestWidget.prototype.updatePipelineUrls = function(id) { - const pipelineUrl = this.opts.pipeline_path; - $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); - }; - - MergeRequestWidget.prototype.updateCommitUrls = function(id) { - const commitsUrl = this.opts.commits_path; - $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); - }; - - MergeRequestWidget.prototype.initMiniPipelineGraph = function() { - new gl.MiniPipelineGraph({ - container: '.js-pipeline-inline-mr-widget-graph:visible', - }).bindEvents(); - }; - - return MergeRequestWidget; - })(); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js b/app/assets/javascripts/merge_request_widget/ci_bundle.js new file mode 100644 index 00000000000..21d7c3e168e --- /dev/null +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js @@ -0,0 +1,53 @@ +/* global merge_request_widget */ + +(() => { + $(() => { + /* TODO: This needs a better home, or should be refactored. It was previously contained + * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, + * but Vue chokes on script tags and prevents their execution. So it was moved here + * temporarily. + * */ + + $(document) + .off('ajax:send', '.accept-mr-form') + .on('ajax:send', '.accept-mr-form', () => { + $('.accept-mr-form :input').disable(); + }); + + $(document) + .off('click', '.accept-merge-request') + .on('click', '.accept-merge-request', () => { + $('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + }); + + $(document) + .off('click', '.merge-when-pipeline-succeeds') + .on('click', '.merge-when-pipeline-succeeds', () => { + $('#merge_when_pipeline_succeeds').val('1'); + }); + + $(document) + .off('click', '.js-merge-dropdown a') + .on('click', '.js-merge-dropdown a', (e) => { + e.preventDefault(); + $(e.target).closest('form').submit(); + }); + if ($('.rebase-in-progress').length) { + merge_request_widget.rebaseInProgress(); + } else if ($('.rebase-mr-form').length) { + $(document) + .off('ajax:send', '.rebase-mr-form') + .on('ajax:send', '.rebase-mr-form', () => { + $('.rebase-mr-form :input').disable(); + }); + + $(document) + .off('click', '.js-rebase-button') + .on('click', '.js-rebase-button', () => { + $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); + }); + } else { + setTimeout(() => merge_request_widget.getMergeStatus(), 200); + } + }); +})(); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 deleted file mode 100644 index 5840916846b..00000000000 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -/* global merge_request_widget */ - -(() => { - $(() => { - /* TODO: This needs a better home, or should be refactored. It was previously contained - * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml, - * but Vue chokes on script tags and prevents their execution. So it was moved here - * temporarily. - * */ - - $(document) - .off('ajax:send', '.accept-mr-form') - .on('ajax:send', '.accept-mr-form', () => { - $('.accept-mr-form :input').disable(); - }); - - $(document) - .off('click', '.accept_merge_request') - .on('click', '.accept_merge_request', () => { - $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); - }); - - $(document) - .off('click', '.merge_when_build_succeeds') - .on('click', '.merge_when_build_succeeds', () => { - $('#merge_when_build_succeeds').val('1'); - }); - - $(document) - .off('click', '.js-merge-dropdown a') - .on('click', '.js-merge-dropdown a', (e) => { - e.preventDefault(); - $(e.target).closest('form').submit(); - }); - if ($('.rebase-in-progress').length) { - merge_request_widget.rebaseInProgress(); - } else if ($('.rebase-mr-form').length) { - $(document) - .off('ajax:send', '.rebase-mr-form') - .on('ajax:send', '.rebase-mr-form', () => { - $('.rebase-mr-form :input').disable(); - }); - - $(document) - .off('click', '.js-rebase-button') - .on('click', '.js-rebase-button', () => { - $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); - }); - } else { - setTimeout(() => merge_request_widget.getMergeStatus(), 200); - } - }); -})(); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 7fbaeec7882..38c673e8907 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -78,7 +78,6 @@ } else { $(element).find('.assignee-icon').empty(); } - return $(element).effect('highlight'); }; function Milestone() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 8df1c8e7f94..40e977df693 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -19,7 +19,7 @@ } $els.each(function(i, dropdown) { - var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; + var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); milestonesUrl = $dropdown.data('milestones'); @@ -29,6 +29,7 @@ showAny = $dropdown.data('show-any'); showMenuAbove = $dropdown.data('showMenuAbove'); showUpcoming = $dropdown.data('show-upcoming'); + showStarted = $dropdown.data('show-started'); useId = $dropdown.data('use-id'); defaultLabel = $dropdown.data('default-label'); issuableId = $dropdown.data('issuable-id'); @@ -39,7 +40,7 @@ $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); if (issueUpdateURL) { - milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); + milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); } @@ -71,6 +72,13 @@ title: 'Upcoming' }); } + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: 'Started' + }); + } if (extraOptions.length) { extraOptions.push('divider'); } @@ -124,18 +132,12 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsStore = gl.issueBoards.ModalStore.store.filter; } if (boardsStore) { boardsStore[$dropdown.data('field-name')] = selected.name; - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { @@ -157,7 +159,7 @@ } $dropdown.trigger('loading.gl.dropdown'); - $loading.fadeIn(); + $loading.removeClass('hidden').fadeIn(); gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) .then(function () { @@ -169,7 +171,7 @@ data = {}; data[abilityName] = {}; data[abilityName].milestone_id = selected != null ? selected : null; - $loading.fadeIn(); + $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); return $.ajax({ type: 'PUT', @@ -181,8 +183,7 @@ $selectbox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.namespace = _this.currentProject.namespace; - data.milestone.path = _this.currentProject.path; + data.milestone.full_path = _this.currentProject.full_path; data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js new file mode 100644 index 00000000000..9c58c465001 --- /dev/null +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -0,0 +1,110 @@ +/* eslint-disable no-new */ +/* global Flash */ + +/** + * In each pipelines table we have a mini pipeline graph for each pipeline. + * + * When we click in a pipeline stage, we need to make an API call to get the + * builds list to render in a dropdown. + * + * The container should be the table element. + * + * The stage icon clicked needs to have the following HTML structure: + * <div class="dropdown"> + * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button> + * <div class="js-builds-dropdown-container dropdown-menu"></div> + * </div> + */ + +export default class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + } + + /** + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. + */ + bindEvents() { + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + } + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(document).on( + 'click', + `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, + (e) => { + e.stopPropagation(); + }, + ); + } + + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); + + dropdownContainer.innerHTML = data; + } + + /** + * For the clicked stage, gets the list of builds. + * + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event + * @return {Promise} + */ + getBuildsList(e) { + const button = e.relatedTarget; + const endpoint = button.dataset.stageEndpoint; + + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + this.stopDropdownClickPropagation(); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); + } + + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } +} diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 deleted file mode 100644 index 2145e531331..00000000000 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable no-new */ -/* global Flash */ - -/** - * In each pipelines table we have a mini pipeline graph for each pipeline. - * - * When we click in a pipeline stage, we need to make an API call to get the - * builds list to render in a dropdown. - * - * The container should be the table element. - * - * The stage icon clicked needs to have the following HTML structure: - * <div class="dropdown"> - * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button> - * <div class="js-builds-dropdown-container dropdown-menu"></div> - * </div> - */ -(() => { - class MiniPipelineGraph { - constructor(opts = {}) { - this.container = opts.container || ''; - this.dropdownListSelector = '.js-builds-dropdown-container'; - this.getBuildsList = this.getBuildsList.bind(this); - } - - /** - * Adds the event listener when the dropdown is opened. - * All dropdown events are fired at the .dropdown-menu's parent element. - */ - bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); - } - - /** - * For the clicked stage, renders the given data in the dropdown list. - * - * @param {HTMLElement} stageContainer - * @param {Object} data - */ - renderBuildsList(stageContainer, data) { - const dropdownContainer = stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-list`, - ); - - dropdownContainer.innerHTML = data; - } - - /** - * For the clicked stage, gets the list of builds. - * - * All dropdown events have a relatedTarget property, - * whose value is the toggling anchor element. - * - * @param {Object} e bootstrap dropdown event - * @return {Promise} - */ - getBuildsList(e) { - const button = e.relatedTarget; - const endpoint = button.dataset.stageEndpoint; - - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { - this.toggleLoading(button); - this.renderBuildsList(button, data.html); - }, - error: () => { - this.toggleLoading(button); - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); - } - - /** - * Toggles the visibility of the loading icon. - * - * @param {HTMLElement} stageContainer - * @return {type} - */ - toggleLoading(stageContainer) { - stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-loading`, - ).classList.toggle('hidden'); - } - } - - window.gl = window.gl || {}; - window.gl.MiniPipelineGraph = MiniPipelineGraph; -})(); diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js new file mode 100644 index 00000000000..b3ce9310417 --- /dev/null +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -0,0 +1,6 @@ +import PrometheusGraph from './prometheus_graph'; + +document.addEventListener('DOMContentLoaded', function onLoad() { + document.removeEventListener('DOMContentLoaded', onLoad, false); + return new PrometheusGraph(); +}, false); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js new file mode 100644 index 00000000000..fcffc11a2df --- /dev/null +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -0,0 +1,335 @@ +/* eslint-disable no-new */ +/* global Flash */ + +import d3 from 'd3'; +import statusCodes from '~/lib/utils/http_status'; +import '../lib/utils/common_utils'; +import '../flash'; + +const prometheusGraphsContainer = '.prometheus-graph'; +const metricsEndpoint = 'metrics.json'; +const timeFormat = d3.time.format('%H:%M'); +const dayFormat = d3.time.format('%b %e, %a'); +const bisectDate = d3.bisector(d => d.time).left; +const extraAddedWidthParent = 100; + +class PrometheusGraph { + + constructor() { + this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; + this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + + extraAddedWidthParent; + this.originalWidth = parentContainerWidth; + this.originalHeight = 400; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = 400 - this.margin.top - this.margin.bottom; + this.backOffRequestCounter = 0; + this.configureGraph(); + this.init(); + } + + createGraph() { + Object.keys(this.data).forEach((key) => { + const value = this.data[key]; + if (value.length > 0) { + this.plotValues(value, key); + } + }); + } + + init() { + this.getData().then((metricsResponse) => { + if (Object.keys(metricsResponse).length === 0) { + new Flash('Empty metrics', 'alert'); + } else { + this.transformData(metricsResponse); + this.createGraph(); + } + }); + } + + plotValues(valuesToPlot, key) { + const x = d3.time.scale() + .range([0, this.width]); + + const y = d3.scale.linear() + .range([this.height, 0]); + + const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; + + const graphSpecifics = this.graphSpecificProperties[key]; + + const chart = d3.select(prometheusGraphContainer) + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.bottom + this.margin.top) + .append('g') + .attr('transform', `translate(${this.margin.left},${this.margin.top})`); + + const axisLabelContainer = d3.select(prometheusGraphContainer) + .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) + .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .append('g') + .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); + + x.domain(d3.extent(valuesToPlot, d => d.time)); + y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); + + const xAxis = d3.svg.axis() + .scale(x) + .ticks(this.commonGraphProperties.axis_no_ticks) + .orient('bottom'); + + const yAxis = d3.svg.axis() + .scale(y) + .ticks(this.commonGraphProperties.axis_no_ticks) + .tickSize(-this.width) + .orient('left'); + + this.createAxisLabelContainers(axisLabelContainer, key); + + chart.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${this.height})`) + .call(xAxis); + + chart.append('g') + .attr('class', 'y-axis') + .call(yAxis); + + const area = d3.svg.area() + .x(d => x(d.time)) + .y0(this.height) + .y1(d => y(d.value)) + .interpolate('linear'); + + const line = d3.svg.line() + .x(d => x(d.time)) + .y(d => y(d.value)); + + chart.append('path') + .datum(valuesToPlot) + .attr('d', area) + .attr('class', 'metric-area') + .attr('fill', graphSpecifics.area_fill_color); + + chart.append('path') + .datum(valuesToPlot) + .attr('class', 'metric-line') + .attr('stroke', graphSpecifics.line_color) + .attr('fill', 'none') + .attr('stroke-width', this.commonGraphProperties.area_stroke_width) + .attr('d', line); + + // Overlay area for the mouseover events + chart.append('rect') + .attr('class', 'prometheus-graph-overlay') + .attr('width', this.width) + .attr('height', this.height) + .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); + } + + // The legends from the metric + createAxisLabelContainers(axisLabelContainer, key) { + const graphSpecifics = this.graphSpecificProperties[key]; + + axisLabelContainer.append('line') + .attr('class', 'label-x-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: this.originalHeight - this.marginLabelContainer.top, + x2: this.originalWidth - this.margin.right, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('line') + .attr('class', 'label-y-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: 0, + x2: 0, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('text-anchor', 'middle') + .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`) + .text(graphSpecifics.graph_legend_title); + + axisLabelContainer.append('rect') + .attr('class', 'rect-axis-text') + .attr('x', (this.originalWidth / 2) - this.margin.right) + .attr('y', this.originalHeight - this.marginLabelContainer.top - 20) + .attr('width', 30) + .attr('height', 80); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', (this.originalWidth / 2) - this.margin.right) + .attr('y', this.originalHeight - this.marginLabelContainer.top) + .attr('dy', '.35em') + .text('Time'); + + // Legends + + // Metric Usage + axisLabelContainer.append('rect') + .attr('x', this.originalWidth - 170) + .attr('y', (this.originalHeight / 2) - 80) + .style('fill', graphSpecifics.area_fill_color) + .attr('width', 20) + .attr('height', 35); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 65) + .text(graphSpecifics.graph_legend_title); + + axisLabelContainer.append('text') + .attr('class', 'text-metric-usage') + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 50); + } + + handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { + const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); + const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); + const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); + const d0 = valuesToPlot[timeValueIndex - 1]; + const d1 = valuesToPlot[timeValueIndex]; + const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; + const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); + const currentTimeCoordinate = x(currentData.time); + const graphSpecifics = this.graphSpecificProperties[key]; + // Remove the current selectors + d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); + d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); + + chart.append('line') + .attr('class', 'selected-metric-line') + .attr({ + x1: currentTimeCoordinate, + y1: y(0), + x2: currentTimeCoordinate, + y2: maxValueMetric, + }); + + chart.append('circle') + .attr('class', 'circle-metric') + .attr('fill', graphSpecifics.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', y(currentData.value)) + .attr('r', this.commonGraphProperties.circle_radius_metric); + + // The little box with text + const rectTextMetric = chart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', currentTimeCoordinate + 10) + .attr('y', maxValueMetric) + .attr('width', this.commonGraphProperties.rect_text_width) + .attr('height', this.commonGraphProperties.rect_text_height); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', currentTimeCoordinate + 35) + .attr('y', maxValueMetric + 35) + .text(timeFormat(currentData.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', currentTimeCoordinate + 15) + .attr('y', maxValueMetric + 15) + .text(dayFormat(currentData.time)); + + // Update the text + d3.select(`${prometheusGraphContainer} .text-metric-usage`) + .text(currentData.value.substring(0, 8)); + } + + configureGraph() { + this.graphSpecificProperties = { + cpu_values: { + area_fill_color: '#edf3fc', + line_color: '#5b99f7', + graph_legend_title: 'CPU Usage (Cores)', + }, + memory_values: { + area_fill_color: '#fca326', + line_color: '#fc6d26', + graph_legend_title: 'Memory Usage (MB)', + }, + }; + + this.commonGraphProperties = { + area_stroke_width: 2, + median_total_characters: 8, + circle_radius_metric: 5, + rect_text_width: 90, + rect_text_height: 40, + axis_no_ticks: 3, + }; + } + + getData() { + const maxNumberOfRequests = 3; + return gl.utils.backOff((next, stop) => { + $.ajax({ + url: metricsEndpoint, + dataType: 'json', + }) + .done((data, statusText, resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < maxNumberOfRequests) { + next(); + } else { + stop({ + status: resp.status, + metrics: data, + }); + } + } else { + stop({ + status: resp.status, + metrics: data, + }); + } + }).fail(stop); + }) + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + return {}; + } + return resp.metrics; + }) + .catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); + } + + transformData(metricsResponse) { + const metricTypes = {}; + Object.keys(metricsResponse.metrics).forEach((key) => { + if (key === 'cpu_values' || key === 'memory_values') { + const metricValues = (metricsResponse.metrics[key])[0]; + metricTypes[key] = metricValues.values.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + } + }); + this.data = metricTypes; + } +} + +export default PrometheusGraph; diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 43dc9838977..5aad3908eb6 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,424 +1,347 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ -/* global Raphael */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +import Raphael from './raphael'; - this.BranchGraph = (function() { - function BranchGraph(element1, options1) { - this.element = element1; - this.options = options1; - this.scrollTop = bind(this.scrollTop, this); - this.scrollBottom = bind(this.scrollBottom, this); - this.scrollRight = bind(this.scrollRight, this); - this.scrollLeft = bind(this.scrollLeft, this); - this.scrollUp = bind(this.scrollUp, this); - this.scrollDown = bind(this.scrollDown, this); - this.preparedCommits = {}; - this.mtime = 0; - this.mspace = 0; - this.parents = {}; - this.colors = ["#000"]; - this.offsetX = 150; - this.offsetY = 20; - this.unitTime = 30; - this.unitSpace = 10; - this.prev_start = -1; - this.load(); - } - - BranchGraph.prototype.load = function() { - return $.ajax({ - url: this.options.url, - method: "get", - dataType: "json", - success: $.proxy(function(data) { - $(".loading", this.element).hide(); - this.prepareData(data.days, data.commits); - return this.buildGraph(); - }, this) - }); - }; +export default (function() { + function BranchGraph(element1, options1) { + this.element = element1; + this.options = options1; + this.scrollTop = this.scrollTop.bind(this); + this.scrollBottom = this.scrollBottom.bind(this); + this.scrollRight = this.scrollRight.bind(this); + this.scrollLeft = this.scrollLeft.bind(this); + this.scrollUp = this.scrollUp.bind(this); + this.scrollDown = this.scrollDown.bind(this); + this.preparedCommits = {}; + this.mtime = 0; + this.mspace = 0; + this.parents = {}; + this.colors = ["#000"]; + this.offsetX = 150; + this.offsetY = 20; + this.unitTime = 30; + this.unitSpace = 10; + this.prev_start = -1; + this.load(); + } - BranchGraph.prototype.prepareData = function(days, commits) { - var c, ch, cw, j, len, ref; - this.days = days; - this.commits = commits; - this.collectParents(); - this.graphHeight = $(this.element).height(); - this.graphWidth = $(this.element).width(); - ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); - cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); - this.r = Raphael(this.element.get(0), cw, ch); - this.top = this.r.set(); - this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); - ref = this.commits; - for (j = 0, len = ref.length; j < len; j += 1) { - c = ref[j]; - if (c.id in this.parents) { - c.isParent = true; - } - this.preparedCommits[c.id] = c; - this.markCommit(c); - } - return this.collectColors(); - }; - - BranchGraph.prototype.collectParents = function() { - var c, j, len, p, ref, results; - ref = this.commits; - results = []; - for (j = 0, len = ref.length; j < len; j += 1) { - c = ref[j]; - this.mtime = Math.max(this.mtime, c.time); - this.mspace = Math.max(this.mspace, c.space); - results.push((function() { - var l, len1, ref1, results1; - ref1 = c.parents; - results1 = []; - for (l = 0, len1 = ref1.length; l < len1; l += 1) { - p = ref1[l]; - this.parents[p[0]] = true; - results1.push(this.mspace = Math.max(this.mspace, p[1])); - } - return results1; - }).call(this)); - } - return results; - }; + BranchGraph.prototype.load = function() { + return $.ajax({ + url: this.options.url, + method: "get", + dataType: "json", + success: $.proxy(function(data) { + $(".loading", this.element).hide(); + this.prepareData(data.days, data.commits); + return this.buildGraph(); + }, this) + }); + }; - BranchGraph.prototype.collectColors = function() { - var k, results; - k = 0; - results = []; - while (k < this.mspace) { - this.colors.push(Raphael.getColor(.8)); - // Skipping a few colors in the spectrum to get more contrast between colors - Raphael.getColor(); - Raphael.getColor(); - results.push(k += 1); + BranchGraph.prototype.prepareData = function(days, commits) { + var c, ch, cw, j, len, ref; + this.days = days; + this.commits = commits; + this.collectParents(); + this.graphHeight = $(this.element).height(); + this.graphWidth = $(this.element).width(); + ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); + cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); + this.r = Raphael(this.element.get(0), cw, ch); + this.top = this.r.set(); + this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); + ref = this.commits; + for (j = 0, len = ref.length; j < len; j += 1) { + c = ref[j]; + if (c.id in this.parents) { + c.isParent = true; } - return results; - }; + this.preparedCommits[c.id] = c; + this.markCommit(c); + } + return this.collectColors(); + }; - BranchGraph.prototype.buildGraph = function() { - var cuday, cumonth, day, j, len, mm, r, ref; - r = this.r; - cuday = 0; - cumonth = ""; - r.rect(0, 0, 40, this.barHeight).attr({ - fill: "#222" - }); - r.rect(40, 0, 30, this.barHeight).attr({ - fill: "#444" - }); - ref = this.days; - for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { - day = ref[mm]; - if (cuday !== day[0] || cumonth !== day[1]) { - // Dates - r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ - font: "12px Monaco, monospace", - fill: "#BBB" - }); - cuday = day[0]; - } - if (cumonth !== day[1]) { - // Months - r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ - font: "12px Monaco, monospace", - fill: "#EEE" - }); - cumonth = day[1]; + BranchGraph.prototype.collectParents = function() { + var c, j, len, p, ref, results; + ref = this.commits; + results = []; + for (j = 0, len = ref.length; j < len; j += 1) { + c = ref[j]; + this.mtime = Math.max(this.mtime, c.time); + this.mspace = Math.max(this.mspace, c.space); + results.push((function() { + var l, len1, ref1, results1; + ref1 = c.parents; + results1 = []; + for (l = 0, len1 = ref1.length; l < len1; l += 1) { + p = ref1[l]; + this.parents[p[0]] = true; + results1.push(this.mspace = Math.max(this.mspace, p[1])); } - } - this.renderPartialGraph(); - return this.bindEvents(); - }; + return results1; + }).call(this)); + } + return results; + }; - BranchGraph.prototype.renderPartialGraph = function() { - var commit, end, i, isGraphEdge, start, x, y; - start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; - if (start < 0) { - isGraphEdge = true; - start = 0; + BranchGraph.prototype.collectColors = function() { + var k, results; + k = 0; + results = []; + while (k < this.mspace) { + this.colors.push(Raphael.getColor(.8)); + // Skipping a few colors in the spectrum to get more contrast between colors + Raphael.getColor(); + Raphael.getColor(); + results.push(k += 1); + } + return results; + }; + + BranchGraph.prototype.buildGraph = function() { + var cuday, cumonth, day, j, len, mm, r, ref; + r = this.r; + cuday = 0; + cumonth = ""; + r.rect(0, 0, 40, this.barHeight).attr({ + fill: "#222" + }); + r.rect(40, 0, 30, this.barHeight).attr({ + fill: "#444" + }); + ref = this.days; + for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { + day = ref[mm]; + if (cuday !== day[0] || cumonth !== day[1]) { + // Dates + r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ + font: "12px Monaco, monospace", + fill: "#BBB" + }); + cuday = day[0]; } - end = start + 40; - if (this.commits.length < end) { - isGraphEdge = true; - end = this.commits.length; + if (cumonth !== day[1]) { + // Months + r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ + font: "12px Monaco, monospace", + fill: "#EEE" + }); + cumonth = day[1]; } - if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { - i = start; - this.prev_start = start; - while (i < end) { - commit = this.commits[i]; - i += 1; - if (commit.hasDrawn !== true) { - x = this.offsetX + this.unitSpace * (this.mspace - commit.space); - y = this.offsetY + this.unitTime * commit.time; - this.drawDot(x, y, commit); - this.drawLines(x, y, commit); - this.appendLabel(x, y, commit); - this.appendAnchor(x, y, commit); - commit.hasDrawn = true; - } + } + this.renderPartialGraph(); + return this.bindEvents(); + }; + + BranchGraph.prototype.renderPartialGraph = function() { + var commit, end, i, isGraphEdge, start, x, y; + start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; + if (start < 0) { + isGraphEdge = true; + start = 0; + } + end = start + 40; + if (this.commits.length < end) { + isGraphEdge = true; + end = this.commits.length; + } + if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { + i = start; + this.prev_start = start; + while (i < end) { + commit = this.commits[i]; + i += 1; + if (commit.hasDrawn !== true) { + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + this.drawDot(x, y, commit); + this.drawLines(x, y, commit); + this.appendLabel(x, y, commit); + this.appendAnchor(x, y, commit); + commit.hasDrawn = true; } - return this.top.toFront(); } - }; - - BranchGraph.prototype.bindEvents = function() { - var element; - element = this.element; - return $(element).scroll((function(_this) { - return function(event) { - return _this.renderPartialGraph(); - }; - })(this)); - }; - - BranchGraph.prototype.scrollDown = function() { - this.element.scrollTop(this.element.scrollTop() + 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollUp = function() { - this.element.scrollTop(this.element.scrollTop() - 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollLeft = function() { - this.element.scrollLeft(this.element.scrollLeft() - 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollRight = function() { - this.element.scrollLeft(this.element.scrollLeft() + 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollBottom = function() { - return this.element.scrollTop(this.element.find('svg').height()); - }; + return this.top.toFront(); + } + }; - BranchGraph.prototype.scrollTop = function() { - return this.element.scrollTop(0); - }; + BranchGraph.prototype.bindEvents = function() { + var element; + element = this.element; + return $(element).scroll((function(_this) { + return function(event) { + return _this.renderPartialGraph(); + }; + })(this)); + }; - BranchGraph.prototype.appendLabel = function(x, y, commit) { - var label, r, rect, shortrefs, text, textbox, triangle; - if (!commit.refs) { - return; - } - r = this.r; - shortrefs = commit.refs; - // Truncate if longer than 15 chars - if (shortrefs.length > 17) { - shortrefs = shortrefs.substr(0, 15) + "…"; - } - text = r.text(x + 4, y, shortrefs).attr({ - "text-anchor": "start", - font: "10px Monaco, monospace", - fill: "#FFF", - title: commit.refs - }); - textbox = text.getBBox(); - // Create rectangle based on the size of the textbox - rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - label = r.set(rect, text); - label.transform(["t", -rect.getBBox().width - 15, 0]); - // Set text to front - return text.toFront(); - }; + BranchGraph.prototype.scrollDown = function() { + this.element.scrollTop(this.element.scrollTop() + 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.appendAnchor = function(x, y, commit) { - var anchor, options, r, top; - r = this.r; - top = this.top; - options = this.options; - anchor = r.circle(x, y, 10).attr({ - fill: "#000", - opacity: 0, - cursor: "pointer" - }).click(function() { - return window.open(options.commit_url.replace("%s", commit.id), "_blank"); - }).hover(function() { - this.tooltip = r.commitTooltip(x + 5, y, commit); - return top.push(this.tooltip.insertBefore(this)); - }, function() { - return this.tooltip && this.tooltip.remove() && delete this.tooltip; - }); - return top.push(anchor); - }; + BranchGraph.prototype.scrollUp = function() { + this.element.scrollTop(this.element.scrollTop() - 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.drawDot = function(x, y, commit) { - var avatar_box_x, avatar_box_y, r; - r = this.r; - r.circle(x, y, 3).attr({ - fill: this.colors[commit.space], - stroke: "none" - }); - avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; - avatar_box_y = y - 10; - r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ - stroke: this.colors[commit.space], - "stroke-width": 2 - }); - r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); - return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ - "text-anchor": "start", - font: "14px Monaco, monospace" - }); - }; + BranchGraph.prototype.scrollLeft = function() { + this.element.scrollLeft(this.element.scrollLeft() - 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.drawLines = function(x, y, commit) { - var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; - r = this.r; - ref = commit.parents; - results = []; - for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { - parent = ref[i]; - parentCommit = this.preparedCommits[parent[0]]; - parentY = this.offsetY + this.unitTime * parentCommit.time; - parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); - parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); - // Set line color - if (parentCommit.space <= commit.space) { - color = this.colors[commit.space]; - } else { - color = this.colors[parentCommit.space]; - } - // Build line shape - if (parent[1] === commit.space) { - offset = [0, 5]; - arrow = "l-2,5,4,0,-2,-5,0,5"; - } else if (parent[1] < commit.space) { - offset = [3, 3]; - arrow = "l5,0,-2,4,-3,-4,4,2"; - } else { - offset = [-3, 3]; - arrow = "l-5,0,2,4,3,-4,-4,2"; - } - // Start point - route = ["M", x + offset[0], y + offset[1]]; - // Add arrow if not first parent - if (i > 0) { - route.push(arrow); - } - // Circumvent if overlap - if (commit.space !== parentCommit.space || commit.space !== parent[1]) { - route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); - } - // End point - route.push("L", parentX1, parentY); - results.push(r.path(route).attr({ - stroke: color, - "stroke-width": 2 - })); - } - return results; - }; + BranchGraph.prototype.scrollRight = function() { + this.element.scrollLeft(this.element.scrollLeft() + 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.markCommit = function(commit) { - var r, x, y; - if (commit.id === this.options.commit_id) { - r = this.r; - x = this.offsetX + this.unitSpace * (this.mspace - commit.space); - y = this.offsetY + this.unitTime * commit.time; - r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - // Displayed in the center - return this.element.scrollTop(y - this.graphHeight / 2); - } - }; + BranchGraph.prototype.scrollBottom = function() { + return this.element.scrollTop(this.element.find('svg').height()); + }; - return BranchGraph; - })(); + BranchGraph.prototype.scrollTop = function() { + return this.element.scrollTop(0); + }; - Raphael.prototype.commitTooltip = function(x, y, commit) { - var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip; - boxWidth = 300; - boxHeight = 200; - icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); - nameText = this.text(x + 25, y + 10, commit.author.name); - idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); - textSet = this.set(icon, nameText, idText, messageText).attr({ + BranchGraph.prototype.appendLabel = function(x, y, commit) { + var label, r, rect, shortrefs, text, textbox, triangle; + if (!commit.refs) { + return; + } + r = this.r; + shortrefs = commit.refs; + // Truncate if longer than 15 chars + if (shortrefs.length > 17) { + shortrefs = shortrefs.substr(0, 15) + "…"; + } + text = r.text(x + 4, y, shortrefs).attr({ "text-anchor": "start", - font: "12px Monaco, monospace" - }); - nameText.attr({ - font: "14px Arial", - "font-weight": "bold" + font: "10px Monaco, monospace", + fill: "#FFF", + title: commit.refs }); - idText.attr({ - fill: "#AAA" + textbox = text.getBBox(); + // Create rectangle based on the size of the textbox + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" }); - messageText.node.style["white-space"] = "pre"; - this.textWrap(messageText, boxWidth - 50); - rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ - fill: "#FFF", - stroke: "#000", - "stroke-linecap": "round", - "stroke-width": 2 + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" }); - tooltip = this.set(rect, textSet); - rect.attr({ - height: tooltip.getBBox().height + 10, - width: tooltip.getBBox().width + 10 + label = r.set(rect, text); + label.transform(["t", -rect.getBBox().width - 15, 0]); + // Set text to front + return text.toFront(); + }; + + BranchGraph.prototype.appendAnchor = function(x, y, commit) { + var anchor, options, r, top; + r = this.r; + top = this.top; + options = this.options; + anchor = r.circle(x, y, 10).attr({ + fill: "#000", + opacity: 0, + cursor: "pointer" + }).click(function() { + return window.open(options.commit_url.replace("%s", commit.id), "_blank"); + }).hover(function() { + this.tooltip = r.commitTooltip(x + 5, y, commit); + return top.push(this.tooltip.insertBefore(this)); + }, function() { + return this.tooltip && this.tooltip.remove() && delete this.tooltip; }); - tooltip.transform(["t", 20, 20]); - return tooltip; + return top.push(anchor); }; - Raphael.prototype.textWrap = function(t, width) { - var abc, b, content, h, j, len, letterWidth, s, word, words, x; - content = t.attr("text"); - abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - t.attr({ - text: abc + BranchGraph.prototype.drawDot = function(x, y, commit) { + var avatar_box_x, avatar_box_y, r; + r = this.r; + r.circle(x, y, 3).attr({ + fill: this.colors[commit.space], + stroke: "none" }); - letterWidth = t.getBBox().width / abc.length; - t.attr({ - text: content + avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; + avatar_box_y = y - 10; + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ + stroke: this.colors[commit.space], + "stroke-width": 2 }); - words = content.split(" "); - x = 0; - s = []; - for (j = 0, len = words.length; j < len; j += 1) { - word = words[j]; - if (x + (word.length * letterWidth) > width) { - s.push("\n"); - x = 0; + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); + return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ + "text-anchor": "start", + font: "14px Monaco, monospace" + }); + }; + + BranchGraph.prototype.drawLines = function(x, y, commit) { + var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; + r = this.r; + ref = commit.parents; + results = []; + for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { + parent = ref[i]; + parentCommit = this.preparedCommits[parent[0]]; + parentY = this.offsetY + this.unitTime * parentCommit.time; + parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); + parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); + // Set line color + if (parentCommit.space <= commit.space) { + color = this.colors[commit.space]; + } else { + color = this.colors[parentCommit.space]; } - if (word === "\n") { - s.push("\n"); - x = 0; + // Build line shape + if (parent[1] === commit.space) { + offset = [0, 5]; + arrow = "l-2,5,4,0,-2,-5,0,5"; + } else if (parent[1] < commit.space) { + offset = [3, 3]; + arrow = "l5,0,-2,4,-3,-4,4,2"; } else { - s.push(word + " "); - x += word.length * letterWidth; + offset = [-3, 3]; + arrow = "l-5,0,2,4,3,-4,-4,2"; + } + // Start point + route = ["M", x + offset[0], y + offset[1]]; + // Add arrow if not first parent + if (i > 0) { + route.push(arrow); + } + // Circumvent if overlap + if (commit.space !== parentCommit.space || commit.space !== parent[1]) { + route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); } + // End point + route.push("L", parentX1, parentY); + results.push(r.path(route).attr({ + stroke: color, + "stroke-width": 2 + })); + } + return results; + }; + + BranchGraph.prototype.markCommit = function(commit) { + var r, x, y; + if (commit.id === this.options.commit_id) { + r = this.r; + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + // Displayed in the center + return this.element.scrollTop(y - this.graphHeight / 2); } - t.attr({ - text: s.join("").trim() - }); - b = t.getBBox(); - h = Math.abs(b.y2) + 1; - return t.attr({ - y: h - }); }; -}).call(window); + + return BranchGraph; +})(); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 8e7027b44e7..a3fd22aff2a 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,20 +1,19 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ -/* global BranchGraph */ -(function() { - this.Network = (function() { - function Network(opts) { - var vph; - $("#filter_ref").click(function() { - return $(this).closest('form').submit(); - }); - this.branch_graph = new BranchGraph($(".network-graph"), opts); - vph = $(window).height() - 250; - $('.network-graph').css({ - 'height': vph + 'px' - }); - } +import BranchGraph from './branch_graph'; - return Network; - })(); -}).call(window); +export default (function() { + function Network(opts) { + var vph; + $("#filter_ref").click(function() { + return $(this).closest('form').submit(); + }); + this.branch_graph = new BranchGraph($(".network-graph"), opts); + vph = $(window).height() - 250; + $('.network-graph').css({ + 'height': vph + 'px' + }); + } + + return Network; +})(); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index aae509caa79..8aae2ad201c 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,22 +1,17 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ -/* global Network */ /* global ShortcutsNetwork */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); +import Network from './network'; -(function() { - $(function() { - if (!$(".network-graph").length) return; +$(function() { + if (!$(".network-graph").length) return; - var network_graph; - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }); - return new ShortcutsNetwork(network_graph.branch_graph); + var network_graph; + network_graph = new Network({ + url: $(".network-graph").attr('data-url'), + commit_url: $(".network-graph").attr('data-commit-url'), + ref: $(".network-graph").attr('data-ref'), + commit_id: $(".network-graph").attr('data-commit-id') }); -}).call(window); + return new ShortcutsNetwork(network_graph.branch_graph); +}); diff --git a/app/assets/javascripts/network/raphael.js b/app/assets/javascripts/network/raphael.js new file mode 100644 index 00000000000..09dcf716148 --- /dev/null +++ b/app/assets/javascripts/network/raphael.js @@ -0,0 +1,74 @@ +import Raphael from 'raphael/raphael'; + +Raphael.prototype.commitTooltip = function commitTooltip(x, y, commit) { + const boxWidth = 300; + const icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); + const nameText = this.text(x + 25, y + 10, commit.author.name); + const idText = this.text(x, y + 35, commit.id); + const messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, ' \n ')); + const textSet = this.set(icon, nameText, idText, messageText).attr({ + 'text-anchor': 'start', + font: '12px Monaco, monospace', + }); + nameText.attr({ + font: '14px Arial', + 'font-weight': 'bold', + }); + idText.attr({ + fill: '#AAA', + }); + messageText.node.style['white-space'] = 'pre'; + this.textWrap(messageText, boxWidth - 50); + const rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ + fill: '#FFF', + stroke: '#000', + 'stroke-linecap': 'round', + 'stroke-width': 2, + }); + const tooltip = this.set(rect, textSet); + rect.attr({ + height: tooltip.getBBox().height + 10, + width: tooltip.getBBox().width + 10, + }); + tooltip.transform(['t', 20, 20]); + return tooltip; +}; + +Raphael.prototype.textWrap = function testWrap(t, width) { + const content = t.attr('text'); + const abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + t.attr({ + text: abc, + }); + const letterWidth = t.getBBox().width / abc.length; + t.attr({ + text: content, + }); + const words = content.split(' '); + let x = 0; + const s = []; + for (let j = 0, len = words.length; j < len; j += 1) { + const word = words[j]; + if (x + (word.length * letterWidth) > width) { + s.push('\n'); + x = 0; + } + if (word === '\n') { + s.push('\n'); + x = 0; + } else { + s.push(`${word} `); + x += word.length * letterWidth; + } + } + t.attr({ + text: s.join('').trim(), + }); + const b = t.getBBox(); + const h = Math.abs(b.y2) + 1; + return t.attr({ + y: h, + }); +}; + +export default Raphael; diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index cb24f212c66..5828f460a23 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -20,15 +20,35 @@ }; NewBranchForm.prototype.init = function() { - if (this.name.val().length > 0) { + if (this.name.length && this.name.val().length > 0) { return this.name.trigger('blur'); } }; NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { - return this.ref.autocomplete({ - source: availableRefs, - minLength: 1 + var $branchSelect = $('.js-branch-select'); + + $branchSelect.glDropdown({ + data: availableRefs, + filterable: true, + filterByText: true, + remote: false, + fieldName: $branchSelect.data('field-name'), + selectable: true, + isSelectable: function(branch, $el) { + return !$el.hasClass('is-active'); + }, + text: function(branch) { + return branch; + }, + id: function(branch) { + return branch; + }, + toggleLabel: function(branch) { + if (branch) { + return branch; + } + } }); }; @@ -61,7 +81,7 @@ var errorMessage, errors, formatter, unique, validator; this.branchNameError.empty(); unique = function(values, value) { - if (indexOf.call(values, value) < 0) { + if (indexOf.call(values, value) === -1) { values.push(value); } return values; diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 747f693726e..ad36f08840d 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -3,19 +3,23 @@ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.NewCommitForm = (function() { - function NewCommitForm(form) { + function NewCommitForm(form, targetBranchName = 'target_branch') { + this.form = form; + this.targetBranchName = targetBranchName; this.renderDestination = bind(this.renderDestination, this); - this.newBranch = form.find('.js-target-branch'); + this.targetBranchDropdown = form.find('button.js-target-branch'); this.originalBranch = form.find('.js-original-branch'); this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); + this.targetBranchDropdown.on('change.branch', this.renderDestination); this.renderDestination(); - this.newBranch.keyup(this.renderDestination); } NewCommitForm.prototype.renderDestination = function() { var different; - different = this.newBranch.val() !== this.originalBranch.val(); + var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`); + + different = targetBranch.val() !== this.originalBranch.val(); if (different) { this.createMergeRequestContainer.show(); if (!this.wasDifferent) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 03504255bda..47cc34e7a20 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,12 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ /* global Autosave */ +/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); +window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho @@ -42,7 +44,6 @@ require('./task_list'); this.notes_url = notes_url; this.note_ids = note_ids; this.last_fetched_at = last_fetched_at; - this.view = view; this.noteable_url = document.URL; this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.basePollingInterval = 15000; @@ -57,6 +58,7 @@ require('./task_list'); selector: '.notes' }); this.collapseLongCommitList(); + this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (gl.utils.getPagePath(1) === 'merge_requests') { @@ -65,6 +67,10 @@ require('./task_list'); } } + Notes.prototype.setViewType = function(view) { + this.view = Cookies.get('diff_view') || view; + }; + Notes.prototype.addBinding = function() { // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); @@ -198,7 +204,7 @@ require('./task_list'); this.refreshing = true; return $.ajax({ url: this.notes_url, - data: "last_fetched_at=" + this.last_fetched_at, + headers: { "X-Last-Fetched-At": this.last_fetched_at }, dataType: "json", success: (function(_this) { return function(data) { @@ -246,12 +252,21 @@ require('./task_list'); }; Notes.prototype.handleCreateChanges = function(note) { + var votesBlock; if (typeof note === 'undefined') { return; } - if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) { - $.get(mrRefreshWidgetUrl); + if (note.commands_changes) { + if ('merge' in note.commands_changes) { + $.get(mrRefreshWidgetUrl); + } + + if ('emoji_award' in note.commands_changes) { + votesBlock = $('.js-awards-block').eq(0); + gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award); + return gl.awardsHandler.scrollToAwards(); + } } }; @@ -262,26 +277,16 @@ require('./task_list'); */ Notes.prototype.renderNote = function(note) { - var $notesList, votesBlock; + var $notesList; if (!note.valid) { - if (note.award) { - new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline); - } - else { - if (note.errors.commands_only) { - new Flash(note.errors.commands_only, 'notice', this.parentTimeline); - this.refresh(); - } + if (note.errors.commands_only) { + new Flash(note.errors.commands_only, 'notice', this.parentTimeline); + this.refresh(); } return; } - if (note.award) { - votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name); - return gl.awardsHandler.scrollToAwards(); - // render note if it not present in loaded list - // or skip if rendered - } else if (this.isNewNote(note)) { + + if (this.isNewNote(note)) { this.note_ids.push(note.id); $notesList = $('ul.main-notes-list'); $notesList.append(note.html).syntaxHighlight(); @@ -303,7 +308,7 @@ require('./task_list'); }; Notes.prototype.isParallelView = function() { - return this.view === 'parallel'; + return Cookies.get('diff_view') === 'parallel'; }; /* @@ -313,7 +318,7 @@ require('./task_list'); */ Notes.prototype.renderDiscussionNote = function(note) { - var discussionContainer, form, note_html, row; + var discussionContainer, form, note_html, row, lineType, diffAvatarContainer; if (!this.isNewNote(note)) { return; } @@ -323,6 +328,8 @@ require('./task_list'); form = $("#new-discussion-note-form-" + note.original_discussion_id); } row = form.closest("tr"); + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; + diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); note_html = $(note.html); note_html.renderGFM(); // is this the first note of discussion? @@ -331,10 +338,26 @@ require('./task_list'); discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); } if (discussionContainer.length === 0) { - // insert the note and the reply button after the temp row - row.after(note.diff_discussion_html); - // remove the note (will be added again below) - row.next().find(".note").remove(); + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + // insert the note and the reply button after the temp row + row.after(note.diff_discussion_html); + + // remove the note (will be added again below) + row.next().find(".note").remove(); + } else { + // Merge new discussion HTML in + var $discussion = $(note.diff_discussion_html); + var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]'); + var contentContainerClass = '.' + $notes.closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + // remove the note (will be added again below) + $notes.find('.note').remove(); + + row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); + } // Before that, the container didn't exist discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); // Add note to 'Changes' page discussions @@ -348,14 +371,40 @@ require('./task_list'); discussionContainer.append(note_html); } - if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) { gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, note); } gl.utils.localTimeAgo($('.js-timeago'), false); return this.updateNotesCount(1); }; + Notes.prototype.getLineHolder = function(changesDiscussionContainer) { + return $(changesDiscussionContainer).closest('.notes_holder') + .prevAll('.line_holder') + .first() + .get(0); + }; + + Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) { + var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); + var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); + + if (!avatarHolder.length) { + avatarHolder = document.createElement('diff-note-avatars'); + avatarHolder.setAttribute('discussion-id', note.discussion_id); + + diffAvatarContainer.append(avatarHolder); + + gl.diffNotesCompileComponents(); + } + + if (commentButton.length) { + commentButton.remove(); + } + }; + /* Called in response the main target form has been successfully submitted. @@ -593,9 +642,14 @@ require('./task_list'); */ Notes.prototype.removeNote = function(e) { - var noteId; - noteId = $(e.currentTarget).closest(".note").attr("id"); - $(".note[id='" + noteId + "']").each((function(_this) { + var noteElId, noteId, dataNoteId, $note, lineHolder; + $note = $(e.currentTarget).closest('.note'); + noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); + $(".note[id='" + noteElId + "']").each((function(_this) { // A same note appears in the "Discussion" and in the "Changes" tab, we have // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, // where $("#noteId") would return only one. @@ -605,17 +659,26 @@ require('./task_list'); notes = note.closest(".notes"); if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteId]) { - gl.diffNoteApps[noteId].$destroy(); + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); } } + note.remove(); + // check if this is the last note for this line - if (notes.find(".note").length === 1) { + if (notes.find(".note").length === 0) { + var notesTr = notes.closest("tr"); + // "Discussions" tab notes.closest(".timeline-entry").remove(); - // "Changes" tab / commit view - notes.closest("tr").remove(); + + if (!_this.isParallelView() || notesTr.find('.note').length === 0) { + // "Changes" tab / commit view + notesTr.remove(); + } else { + notes.closest('.content').empty(); + } } return note.remove(); }; @@ -708,15 +771,16 @@ require('./task_list'); */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; e.preventDefault(); - $link = $(e.currentTarget); + $link = $(e.currentTarget || e.target); row = $link.closest("tr"); nextRow = row.next(); hasNotes = nextRow.is(".notes_holder"); addForm = false; notesContentSelector = ".notes_content"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; + isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar'); // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineType = $link.data("lineType"); @@ -724,7 +788,9 @@ require('./task_list'); rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>"; } notesContentSelector += " .content"; - if (hasNotes) { + notesContent = nextRow.find(notesContentSelector); + + if (hasNotes && !isDiffCommentAvatar) { nextRow.show(); notesContent = nextRow.find(notesContentSelector); if (notesContent.length) { @@ -741,13 +807,21 @@ require('./task_list'); } } } - } else { + } else if (!isDiffCommentAvatar) { // add a notes row and insert the form row.after(rowCssToAdd); nextRow = row.next(); notesContent = nextRow.find(notesContentSelector); addForm = true; + } else { + nextRow.show(); + notesContent.toggle(!notesContent.is(':visible')); + + if (!nextRow.find('.content:not(:empty)').is(':visible')) { + nextRow.hide(); + } } + if (addForm) { newForm = this.formClone.clone(); newForm.appendTo(notesContent); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js new file mode 100644 index 00000000000..5f6bc902cf8 --- /dev/null +++ b/app/assets/javascripts/pager.js @@ -0,0 +1,77 @@ +require('~/lib/utils/common_utils'); +require('~/lib/utils/url_utility'); + +(() => { + const ENDLESS_SCROLL_BOTTOM_PX = 400; + const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; + + const Pager = { + init(limit = 0, preload = false, disable = false, callback = $.noop) { + this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); + this.limit = limit; + this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; + this.disable = disable; + this.callback = callback; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } + this.initLoadMore(); + }, + + getOld() { + this.loading.show(); + $.ajax({ + type: 'GET', + url: this.url, + data: `limit=${this.limit}&offset=${this.offset}`, + dataType: 'json', + error: () => this.loading.hide(), + success: (data) => { + this.append(data.count, data.html); + this.callback(); + + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }, + }); + }, + + append(count, html) { + $('.content_list').append(html); + if (count > 0) { + this.offset += count; + } else { + this.disable = true; + } + }, + + isScrollable() { + const $w = $(window); + return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; + }, + + initLoadMore() { + $(document).unbind('scroll'); + $(document).endlessScroll({ + bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, + fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, + fireOnce: true, + ceaseFire: () => this.disable === true, + callback: () => { + if (!this.loading.is(':visible')) { + this.loading.show(); + this.getOld(); + } + }, + }); + }, + }; + + window.Pager = Pager; +})(); diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6 deleted file mode 100644 index e35cf6d295e..00000000000 --- a/app/assets/javascripts/pager.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -(() => { - const ENDLESS_SCROLL_BOTTOM_PX = 400; - const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; - - const Pager = { - init(limit = 0, preload = false, disable = false, callback = $.noop) { - this.limit = limit; - this.offset = this.limit; - this.disable = disable; - this.callback = callback; - this.loading = $('.loading').first(); - if (preload) { - this.offset = 0; - this.getOld(); - } - this.initLoadMore(); - }, - - getOld() { - this.loading.show(); - $.ajax({ - type: 'GET', - url: $('.content_list').data('href') || window.location.href, - data: `limit=${this.limit}&offset=${this.offset}`, - dataType: 'json', - error: () => this.loading.hide(), - success: (data) => { - this.append(data.count, data.html); - this.callback(); - - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } - }, - }); - }, - - append(count, html) { - $('.content_list').append(html); - if (count > 0) { - this.offset += count; - } else { - this.disable = true; - } - }, - - isScrollable() { - const $w = $(window); - return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; - }, - - initLoadMore() { - $(document).unbind('scroll'); - $(document).endlessScroll({ - bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, - fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, - fireOnce: true, - ceaseFire: () => this.disable === true, - callback: () => { - if (!this.loading.is(':visible')) { - this.loading.show(); - this.getOld(); - } - }, - }); - }, - }; - - window.Pager = Pager; -})(); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js index 9203abefbbc..9203abefbbc 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js new file mode 100644 index 00000000000..cf1566eeb87 --- /dev/null +++ b/app/assets/javascripts/profile/gl_crop.js @@ -0,0 +1,173 @@ +/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ + +import 'vendor/cropper'; + +((global) => { + // Matches everything but the file name + const FILENAMEREGEX = /^.*[\\\/]/; + + class GitLabCrop { + constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, + exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { + this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); + this.onModalHide = this.onModalHide.bind(this); + this.onModalShow = this.onModalShow.bind(this); + this.onPickImageClick = this.onPickImageClick.bind(this); + this.fileInput = $(input); + this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; + this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`); + this.exportWidth = exportWidth; + this.exportHeight = exportHeight; + this.cropBoxWidth = cropBoxWidth; + this.cropBoxHeight = cropBoxHeight; + this.form = this.fileInput.parents('form'); + this.filename = filename; + this.previewImage = previewImage; + this.modalCrop = modalCrop; + this.pickImageEl = pickImageEl; + this.uploadImageBtn = uploadImageBtn; + this.modalCropImg = modalCropImg; + this.filename = this.getElement(filename); + this.previewImage = this.getElement(previewImage); + this.pickImageEl = this.getElement(pickImageEl); + this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; + this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; + this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; + this.cropActionsBtn = this.modalCrop.find('[data-method]'); + this.bindEvents(); + } + + getElement(selector) { + return $(selector, this.form); + } + + bindEvents() { + var _this; + _this = this; + this.fileInput.on('change', function(e) { + return _this.onFileInputChange(e, this); + }); + this.pickImageEl.on('click', this.onPickImageClick); + this.modalCrop.on('shown.bs.modal', this.onModalShow); + this.modalCrop.on('hidden.bs.modal', this.onModalHide); + this.uploadImageBtn.on('click', this.onUploadImageBtnClick); + this.cropActionsBtn.on('click', function(e) { + var btn; + btn = this; + return _this.onActionBtnClick(btn); + }); + return this.croppedImageBlob = null; + } + + onPickImageClick() { + return this.fileInput.trigger('click'); + } + + onModalShow() { + var _this; + _this = this; + return this.modalCropImg.cropper({ + viewMode: 1, + center: false, + aspectRatio: 1, + modal: true, + scalable: false, + rotatable: false, + zoomable: true, + dragMode: 'move', + guides: false, + zoomOnTouch: false, + zoomOnWheel: false, + cropBoxMovable: false, + cropBoxResizable: false, + toggleDragModeOnDblclick: false, + built: function() { + var $image, container, cropBoxHeight, cropBoxWidth; + $image = $(this); + container = $image.cropper('getContainerData'); + cropBoxWidth = _this.cropBoxWidth; + cropBoxHeight = _this.cropBoxHeight; + return $image.cropper('setCropBoxData', { + width: cropBoxWidth, + height: cropBoxHeight, + left: (container.width - cropBoxWidth) / 2, + top: (container.height - cropBoxHeight) / 2 + }); + } + }); + } + + onModalHide() { + return this.modalCropImg.attr('src', '').cropper('destroy'); + } + + onUploadImageBtnClick(e) { + e.preventDefault(); + this.setBlob(); + this.setPreview(); + this.modalCrop.modal('hide'); + return this.fileInput.val(''); + } + + onActionBtnClick(btn) { + var data, result; + data = $(btn).data(); + if (this.modalCropImg.data('cropper') && data.method) { + return result = this.modalCropImg.cropper(data.method, data.option); + } + } + + onFileInputChange(e, input) { + return this.readFile(input); + } + + readFile(input) { + var _this, reader; + _this = this; + reader = new FileReader; + reader.onload = () => { + _this.modalCropImg.attr('src', reader.result); + return _this.modalCrop.modal('show'); + }; + return reader.readAsDataURL(input.files[0]); + } + + dataURLtoBlob(dataURL) { + var array, binary, i, k, len, v; + binary = atob(dataURL.split(',')[1]); + array = []; + for (k = i = 0, len = binary.length; i < len; k = (i += 1)) { + v = binary[k]; + array.push(binary.charCodeAt(k)); + } + return new Blob([new Uint8Array(array)], { + type: 'image/png' + }); + } + + setPreview() { + var filename; + this.previewImage.attr('src', this.dataURL); + filename = this.fileInput.val().replace(FILENAMEREGEX, ''); + return this.filename.text(filename); + } + + setBlob() { + this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { + width: 200, + height: 200 + }).toDataURL('image/png'); + return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + } + + getBlob() { + return this.croppedImageBlob; + } + } + + $.fn.glCrop = function(opts) { + return this.each(function() { + return $(this).data('glcrop', new GitLabCrop(this, opts)); + }); + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 deleted file mode 100644 index 42e9847af91..00000000000 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ /dev/null @@ -1,171 +0,0 @@ -/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ - -((global) => { - // Matches everything but the file name - const FILENAMEREGEX = /^.*[\\\/]/; - - class GitLabCrop { - constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, - exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { - this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); - this.onModalHide = this.onModalHide.bind(this); - this.onModalShow = this.onModalShow.bind(this); - this.onPickImageClick = this.onPickImageClick.bind(this); - this.fileInput = $(input); - this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; - this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`); - this.exportWidth = exportWidth; - this.exportHeight = exportHeight; - this.cropBoxWidth = cropBoxWidth; - this.cropBoxHeight = cropBoxHeight; - this.form = this.fileInput.parents('form'); - this.filename = filename; - this.previewImage = previewImage; - this.modalCrop = modalCrop; - this.pickImageEl = pickImageEl; - this.uploadImageBtn = uploadImageBtn; - this.modalCropImg = modalCropImg; - this.filename = this.getElement(filename); - this.previewImage = this.getElement(previewImage); - this.pickImageEl = this.getElement(pickImageEl); - this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; - this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; - this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; - this.cropActionsBtn = this.modalCrop.find('[data-method]'); - this.bindEvents(); - } - - getElement(selector) { - return $(selector, this.form); - } - - bindEvents() { - var _this; - _this = this; - this.fileInput.on('change', function(e) { - return _this.onFileInputChange(e, this); - }); - this.pickImageEl.on('click', this.onPickImageClick); - this.modalCrop.on('shown.bs.modal', this.onModalShow); - this.modalCrop.on('hidden.bs.modal', this.onModalHide); - this.uploadImageBtn.on('click', this.onUploadImageBtnClick); - this.cropActionsBtn.on('click', function(e) { - var btn; - btn = this; - return _this.onActionBtnClick(btn); - }); - return this.croppedImageBlob = null; - } - - onPickImageClick() { - return this.fileInput.trigger('click'); - } - - onModalShow() { - var _this; - _this = this; - return this.modalCropImg.cropper({ - viewMode: 1, - center: false, - aspectRatio: 1, - modal: true, - scalable: false, - rotatable: false, - zoomable: true, - dragMode: 'move', - guides: false, - zoomOnTouch: false, - zoomOnWheel: false, - cropBoxMovable: false, - cropBoxResizable: false, - toggleDragModeOnDblclick: false, - built: function() { - var $image, container, cropBoxHeight, cropBoxWidth; - $image = $(this); - container = $image.cropper('getContainerData'); - cropBoxWidth = _this.cropBoxWidth; - cropBoxHeight = _this.cropBoxHeight; - return $image.cropper('setCropBoxData', { - width: cropBoxWidth, - height: cropBoxHeight, - left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 - }); - } - }); - } - - onModalHide() { - return this.modalCropImg.attr('src', '').cropper('destroy'); - } - - onUploadImageBtnClick(e) { - e.preventDefault(); - this.setBlob(); - this.setPreview(); - this.modalCrop.modal('hide'); - return this.fileInput.val(''); - } - - onActionBtnClick(btn) { - var data, result; - data = $(btn).data(); - if (this.modalCropImg.data('cropper') && data.method) { - return result = this.modalCropImg.cropper(data.method, data.option); - } - } - - onFileInputChange(e, input) { - return this.readFile(input); - } - - readFile(input) { - var _this, reader; - _this = this; - reader = new FileReader; - reader.onload = () => { - _this.modalCropImg.attr('src', reader.result); - return _this.modalCrop.modal('show'); - }; - return reader.readAsDataURL(input.files[0]); - } - - dataURLtoBlob(dataURL) { - var array, binary, i, k, len, v; - binary = atob(dataURL.split(',')[1]); - array = []; - for (k = i = 0, len = binary.length; i < len; k = (i += 1)) { - v = binary[k]; - array.push(binary.charCodeAt(k)); - } - return new Blob([new Uint8Array(array)], { - type: 'image/png' - }); - } - - setPreview() { - var filename; - this.previewImage.attr('src', this.dataURL); - filename = this.fileInput.val().replace(FILENAMEREGEX, ''); - return this.filename.text(filename); - } - - setBlob() { - this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { - width: 200, - height: 200 - }).toDataURL('image/png'); - return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); - } - - getBlob() { - return this.croppedImageBlob; - } - } - - $.fn.glCrop = function(opts) { - return this.each(function() { - return $(this).data('glcrop', new GitLabCrop(this, opts)); - }); - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js new file mode 100644 index 00000000000..c38bc762675 --- /dev/null +++ b/app/assets/javascripts/profile/profile.js @@ -0,0 +1,99 @@ +/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ +/* global Flash */ + +((global) => { + class Profile { + constructor({ form } = {}) { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.form = form || $('.edit-user'); + this.bindEvents(); + this.initAvatarGlCrop(); + } + + initAvatarGlCrop() { + const cropOpts = { + filename: '.js-avatar-filename', + previewImage: '.avatar-image .avatar', + modalCrop: '.modal-profile-crop', + pickImageEl: '.js-choose-user-avatar-button', + uploadImageBtn: '.js-upload-user-avatar', + modalCropImg: '.modal-profile-crop-image' + }; + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); + } + + bindEvents() { + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); + $('#user_notification_email').on('change', this.submitForm); + $('.update-username').on('ajax:before', this.beforeUpdateUsername); + $('.update-username').on('ajax:complete', this.afterUpdateUsername); + $('.update-notifications').on('ajax:success', this.onUpdateNotifs); + this.form.on('submit', this.onSubmitForm); + } + + submitForm() { + return $(this).parents('form').submit(); + } + + onSubmitForm(e) { + e.preventDefault(); + return this.saveForm(); + } + + beforeUpdateUsername() { + $('.loading-username', this).removeClass('hidden'); + } + + afterUpdateUsername() { + $('.loading-username', this).addClass('hidden'); + $('button[type=submit]', this).enable(); + } + + onUpdateNotifs(e, data) { + return data.saved ? + new Flash("Notification settings saved", "notice") : + new Flash("Failed to save new settings", "alert"); + } + + saveForm() { + const self = this; + const formData = new FormData(this.form[0]); + const avatarBlob = this.avatarGlCrop.getBlob(); + + if (avatarBlob != null) { + formData.append('user[avatar]', avatarBlob, 'avatar.png'); + } + + return $.ajax({ + url: this.form.attr('action'), + type: this.form.attr('method'), + data: formData, + dataType: "json", + processData: false, + contentType: false, + success: response => new Flash(response.message, 'notice'), + error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), + complete: () => { + window.scrollTo(0, 0); + // Enable submit button after requests ends + return self.form.find(':input[disabled]').enable(); + } + }); + } + } + + $(function() { + $(document).on('input.ssh_key', '#key_key', function() { + const $title = $('#key_title'); + const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + + // Extract the SSH Key title from its comment + if (comment && comment.length > 1) { + return $title.val(comment[1]).change(); + } + }); + if (global.utils.getPagePath() === 'profiles') { + return new Profile(); + } + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 deleted file mode 100644 index 81374296522..00000000000 --- a/app/assets/javascripts/profile/profile.js.es6 +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ -/* global Flash */ - -((global) => { - class Profile { - constructor({ form } = {}) { - this.onSubmitForm = this.onSubmitForm.bind(this); - this.form = form || $('.edit-user'); - this.bindEvents(); - this.initAvatarGlCrop(); - } - - initAvatarGlCrop() { - const cropOpts = { - filename: '.js-avatar-filename', - previewImage: '.avatar-image .avatar', - modalCrop: '.modal-profile-crop', - pickImageEl: '.js-choose-user-avatar-button', - uploadImageBtn: '.js-upload-user-avatar', - modalCropImg: '.modal-profile-crop-image' - }; - this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); - } - - bindEvents() { - $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); - $('#user_notification_email').on('change', this.submitForm); - $('#user_notified_of_own_activity').on('change', this.submitForm); - $('.update-username').on('ajax:before', this.beforeUpdateUsername); - $('.update-username').on('ajax:complete', this.afterUpdateUsername); - $('.update-notifications').on('ajax:success', this.onUpdateNotifs); - this.form.on('submit', this.onSubmitForm); - } - - submitForm() { - return $(this).parents('form').submit(); - } - - onSubmitForm(e) { - e.preventDefault(); - return this.saveForm(); - } - - beforeUpdateUsername() { - $('.loading-username', this).removeClass('hidden'); - } - - afterUpdateUsername() { - $('.loading-username', this).addClass('hidden'); - $('button[type=submit]', this).enable(); - } - - onUpdateNotifs(e, data) { - return data.saved ? - new Flash("Notification settings saved", "notice") : - new Flash("Failed to save new settings", "alert"); - } - - saveForm() { - const self = this; - const formData = new FormData(this.form[0]); - const avatarBlob = this.avatarGlCrop.getBlob(); - - if (avatarBlob != null) { - formData.append('user[avatar]', avatarBlob, 'avatar.png'); - } - - return $.ajax({ - url: this.form.attr('action'), - type: this.form.attr('method'), - data: formData, - dataType: "json", - processData: false, - contentType: false, - success: response => new Flash(response.message, 'notice'), - error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'), - complete: () => { - window.scrollTo(0, 0); - // Enable submit button after requests ends - return self.form.find(':input[disabled]').enable(); - } - }); - } - } - - $(function() { - $(document).on('focusout.ssh_key', '#key_key', function() { - const $title = $('#key_title'); - const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); - if (comment && comment.length > 1 && $title.val() === '') { - return $title.val(comment[1]).change(); - } - // Extract the SSH Key title from its comment - }); - if (global.utils.getPagePath() === 'profiles') { - return new Profile(); - } - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index d7f3c9fd37e..15d32825583 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,3 +1,2 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); +require('./gl_crop'); +require('./profile'); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7c03c8b72d4..db7ceaa2421 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -116,7 +116,7 @@ if ($('input[name="ref"]').length) { var $form = $dropdown.closest('form'); var action = $form.attr('action'); - var divider = action.indexOf('?') < 0 ? '?' : '&'; + var divider = action.indexOf('?') === -1 ? '?' : '&'; gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js index 0a811627600..0a811627600 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index f80e765ce30..3c1c1e7dceb 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -35,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, orderBy, projectsCallback); + return Api.projects(term, { order_by: orderBy }, projectsCallback); } }, url: function(project) { @@ -84,7 +84,7 @@ if (_this.groupId) { return Api.groupProjects(_this.groupId, query.term, projectsCallback); } else { - return Api.projects(query.term, _this.orderBy, projectsCallback); + return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); } }; })(this), diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js index 4ee2e49306d..4ee2e49306d 100644 --- a/app/assets/javascripts/project_variables.js.es6 +++ b/app/assets/javascripts/project_variables.js diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index acdf9b7eb5a..c67d59d2be5 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,50 +1,18 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */ +import FilterableList from './filterable_list'; -(function() { - window.ProjectsList = { - init: function() { - $(".projects-list-filter").off('keyup'); - this.initSearch(); - return this.initPagination(); - }, - initSearch: function() { - var debounceFilter, projectsListFilter; - projectsListFilter = $('.projects-list-filter'); - debounceFilter = _.debounce(window.ProjectsList.filterResults, 500); - return projectsListFilter.on('keyup', function(e) { - if (projectsListFilter.val() !== '') { - return debounceFilter(); - } - }); - }, - filterResults: function() { - var form, project_filter_url, search; - $('.projects-list-holder').fadeTo(250, 0.5); - form = null; - form = $("form#project-filter-form"); - search = $(".projects-list-filter").val(); - project_filter_url = form.attr('action') + '?' + form.serialize(); - return $.ajax({ - type: "GET", - url: form.attr('action'), - data: form.serialize(), - complete: function() { - return $('.projects-list-holder').fadeTo(250, 1); - }, - success: function(data) { - $('.projects-list-holder').replaceWith(data.html); - return history.replaceState({ - page: project_filter_url - // Change url so if user reload a page - search results are saved - }, document.title, project_filter_url); - }, - dataType: "json" - }); - }, - initPagination: function() { - return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) { - return $('.projects-list-holder').replaceWith(data.html); - }); +/** + * Makes search request for projects when user types a value in the search input. + * Updates the html content of the page with the received one. + */ +export default class ProjectsList { + constructor() { + const form = document.querySelector('form#project-filter-form'); + const filter = document.querySelector('.js-projects-list-filter'); + const holder = document.querySelector('.js-projects-list-holder'); + + if (form && filter && holder) { + const list = new FilterableList(form, filter, holder); + list.initSearch(); } - }; -}).call(window); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js index e7fff57ff45..e7fff57ff45 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js index 57ea2f52814..57ea2f52814 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js index 5cf28aa7a73..5cf28aa7a73 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js new file mode 100644 index 00000000000..6ef59e94384 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -0,0 +1,69 @@ +/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ +/* global Flash */ + +(global => { + global.gl = global.gl || {}; + + gl.ProtectedBranchEdit = class { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Allowed to push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + // Do not update if one dropdown has not selected any option + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + + this.$allowedToMergeDropdown.disable(); + this.$allowedToPushDropdown.disable(); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_branch: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), + access_level: $allowedToMergeInput.val() + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val() + }] + } + }, + error() { + $.scrollTo(0); + new Flash('Failed to update branch!'); + } + }).always(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }); + } + }; +})(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 deleted file mode 100644 index 149e511451e..00000000000 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ -/* global Flash */ - -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchEdit = class { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - this.buildDropdowns(); - } - - buildDropdowns() { - // Allowed to merge dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Allowed to push dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - } - - onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); - - // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; - - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), - access_level: $allowedToMergeInput.val() - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val() - }] - } - }, - success: () => { - this.$wrap.effect('highlight'); - }, - error() { - $.scrollTo(0); - new Flash('Failed to update branch!'); - } - }); - } - }; -})(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js index 336fa6c57a7..336fa6c57a7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index ffb66caf5f4..849c1e31623 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,3 +1,5 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); +require('./protected_branch_access_dropdown'); +require('./protected_branch_create'); +require('./protected_branch_dropdown'); +require('./protected_branch_edit'); +require('./protected_branch_edit_list'); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index e66418beeab..15f5963353a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -47,7 +47,7 @@ fields: ['name'] }, data: function(term, callback) { - return Api.projects(term, 'id', function(data) { + return Api.projects(term, { order_by: 'id' }, function(data) { data.unshift({ name_with_namespace: 'Any' }); diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js index 6fd5345a0a6..6fd5345a0a6 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js index bfe90aef71e..bfe90aef71e 100644 --- a/app/assets/javascripts/shortcuts_blob.js.es6 +++ b/app/assets/javascripts/shortcuts_blob.js diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 542cd586df0..09a58cad2b2 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -32,7 +32,7 @@ require('./shortcuts'); return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); }); Mousetrap.bind('g g', function() { - return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'); + return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts'); }); Mousetrap.bind('g i', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js index d811d1cd53a..d811d1cd53a 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js.es6 +++ b/app/assets/javascripts/signin_tabs_memoizer.js diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js index d1bdc353be2..d1bdc353be2 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 89822246bb8..a98403f4cf2 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,10 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); - (function() { $(function() { var editor = ace.edit("editor"); diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js index 2128007113f..2128007113f 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js index d8191605128..d8191605128 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js index 62d1604fe9e..62d1604fe9e 100644 --- a/app/assets/javascripts/subscription.js.es6 +++ b/app/assets/javascripts/subscription.js diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index dfe24d1fb33..b1402c0a880 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,3 +1,4 @@ +/* global Flash */ require('vendor/task_list'); class TaskList { @@ -6,6 +7,16 @@ class TaskList { this.dataType = options.dataType; this.fieldName = options.fieldName; this.onSuccess = options.onSuccess || (() => {}); + this.onError = function showFlash(response) { + let errorMessages = ''; + + if (response.responseJSON) { + errorMessages = response.responseJSON.errors.join(' '); + } + + return new Flash(errorMessages || 'Update failed', 'alert'); + }; + this.init(); } @@ -32,6 +43,7 @@ class TaskList { url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), data: patchData, success: this.onSuccess, + error: this.onError, }); } } diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js index e9e9aafd71a..e9e9aafd71a 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js index 97f6d37364d..97f6d37364d 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js index 6b9422b1816..6b9422b1816 100644 --- a/app/assets/javascripts/terminal/terminal.js.es6 +++ b/app/assets/javascripts/terminal/terminal.js diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js index 13cf3a10a38..13cf3a10a38 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js.es6 +++ b/app/assets/javascripts/terminal/terminal_bundle.js diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index 7dba5840c8a..d48f2404fa5 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -43,7 +43,14 @@ return event; } - function getTraget(target) { + function isLast(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return children.length - 1 === target.index; + } + + function getTarget(target) { var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; var children = el.children; @@ -75,12 +82,22 @@ function simulateDrag(options, callback) { options.to.el = options.to.el || options.from.el; - var fromEl = getTraget(options.from); - var toEl = getTraget(options.to); + var fromEl = getTarget(options.from); + var toEl = getTarget(options.to); + var firstEl = getTarget({ + el: options.to.el, + index: 'first' + }); + var lastEl = getTarget({ + el: options.to.el, + index: 'last' + }); var scrollable = options.scrollable; var fromRect = getRect(fromEl); var toRect = getRect(toEl); + var firstRect = getRect(firstEl); + var lastRect = getRect(lastEl); var startTime = new Date().getTime(); var duration = options.duration || 1000; @@ -88,6 +105,12 @@ options.ontap && options.ontap(); window.SIMULATE_DRAG_ACTIVE = 1; + if (options.to.index === 0) { + toRect.cy = firstRect.y; + } else if (isLast(options.to)) { + toRect.cy = lastRect.y + lastRect.h + 50; + } + var dragInterval = setInterval(function loop() { var progress = (new Date().getTime() - startTime) / duration; var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js new file mode 100644 index 00000000000..8be58023c84 --- /dev/null +++ b/app/assets/javascripts/todos.js @@ -0,0 +1,163 @@ +/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ +/* global UsersSelect */ + +class Todos { + constructor() { + this.initFilters(); + this.bindEvents(); + this.todo_ids = []; + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } + + unbindEvents() { + $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper); + $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper); + $('.todo').off('click', this.goToTodoUrl); + } + + bindEvents() { + this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this); + this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this); + + $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper); + $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper); + $('.todo').on('click', this.goToTodoUrl); + } + + initFilters() { + this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); + this.initFilterDropdown($('.js-type-search'), 'type'); + this.initFilterDropdown($('.js-action-search'), 'action_id'); + + $('form.filter-form').on('submit', function applyFilters(event) { + event.preventDefault(); + gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); + }); + return new UsersSelect(); + } + + initFilterDropdown($dropdown, fieldName, searchFields) { + $dropdown.glDropdown({ + fieldName, + selectable: true, + filterable: searchFields ? true : false, + search: { fields: searchFields }, + data: $dropdown.data('data'), + clicked: () => $dropdown.closest('form.filter-form').submit(), + }); + } + + updateRowStateClicked(e) { + e.preventDefault(); + + const target = e.target; + target.setAttribute('disabled', true); + target.classList.add('disabled'); + $.ajax({ + type: 'POST', + url: target.dataset.href, + dataType: 'json', + data: { + '_method': target.dataset.method, + }, + success: (data) => { + this.updateRowState(target); + return this.updateBadges(data); + }, + }); + } + + updateRowState(target) { + const row = target.closest('li'); + const restoreBtn = row.querySelector('.js-undo-todo'); + const doneBtn = row.querySelector('.js-done-todo'); + + target.classList.add('hidden'); + target.removeAttribute('disabled'); + target.classList.remove('disabled'); + + if (target === doneBtn) { + row.classList.add('done-reversible'); + restoreBtn.classList.remove('hidden'); + } else if (target === restoreBtn) { + row.classList.remove('done-reversible'); + doneBtn.classList.remove('hidden'); + } else { + row.parentNode.removeChild(row); + } + } + + updateAllStateClicked(e) { + e.preventDefault(); + + const target = e.currentTarget; + const requestData = { '_method': target.dataset.method, ids: this.todo_ids }; + target.setAttribute('disabled', true); + target.classList.add('disabled'); + $.ajax({ + type: 'POST', + url: target.dataset.href, + dataType: 'json', + data: requestData, + success: (data) => { + this.updateAllState(target, data); + return this.updateBadges(data); + }, + }); + } + + updateAllState(target, data) { + const markAllDoneBtn = document.querySelector('.js-todos-mark-all'); + const undoAllBtn = document.querySelector('.js-todos-undo-all'); + const todoListContainer = document.querySelector('.js-todos-list-container'); + const nothingHereContainer = document.querySelector('.js-nothing-here-container'); + + target.removeAttribute('disabled'); + target.classList.remove('disabled'); + + this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : []; + undoAllBtn.classList.toggle('hidden'); + markAllDoneBtn.classList.toggle('hidden'); + todoListContainer.classList.toggle('hidden'); + nothingHereContainer.classList.toggle('hidden'); + } + + updateBadges(data) { + $(document).trigger('todo:toggle', data.count); + document.querySelector('.todos-pending .badge').innerHTML = data.count; + document.querySelector('.todos-done .badge').innerHTML = data.done_count; + } + + goToTodoUrl(e) { + const todoLink = this.dataset.url; + + if (!todoLink) { + return; + } + + if (gl.utils.isMetaClick(e)) { + const windowTarget = '_blank'; + const selected = e.target; + e.preventDefault(); + + if (selected.tagName === 'IMG') { + const avatarUrl = selected.parentElement.getAttribute('href'); + window.open(avatarUrl, windowTarget); + } else { + window.open(todoLink, windowTarget); + } + } else { + gl.utils.visitUrl(todoLink); + } + } +} + +window.gl = window.gl || {}; +gl.Todos = Todos; diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 deleted file mode 100644 index e9513725d9d..00000000000 --- a/app/assets/javascripts/todos.js.es6 +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */ -/* global UsersSelect */ - -((global) => { - class Todos { - constructor() { - this.initFilters(); - this.bindEvents(); - - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - - cleanup() { - this.unbindEvents(); - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - unbindEvents() { - $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper); - $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); - $('.todo').off('click', this.goToTodoUrl); - } - - bindEvents() { - this.updateStateClickedWrapper = this.updateStateClicked.bind(this); - this.allDoneClickedWrapper = this.allDoneClicked.bind(this); - - $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper); - $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); - $('.todo').on('click', this.goToTodoUrl); - } - - initFilters() { - new UsersSelect(); - this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); - this.initFilterDropdown($('.js-type-search'), 'type'); - this.initFilterDropdown($('.js-action-search'), 'action_id'); - - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); - }); - } - - initFilterDropdown($dropdown, fieldName, searchFields) { - $dropdown.glDropdown({ - fieldName, - selectable: true, - filterable: searchFields ? true : false, - search: { fields: searchFields }, - data: $dropdown.data('data'), - clicked: function () { - return $dropdown.closest('form.filter-form').submit(); - }, - }); - } - - updateStateClicked(e) { - e.preventDefault(); - const target = e.target; - target.setAttribute('disabled', ''); - target.classList.add('disabled'); - $.ajax({ - type: 'POST', - url: target.getAttribute('href'), - dataType: 'json', - data: { - '_method': target.getAttribute('data-method'), - }, - success: (data) => { - this.updateState(target); - this.updateBadges(data); - }, - }); - } - - allDoneClicked(e) { - e.preventDefault(); - const $target = $(e.currentTarget); - $target.disable(); - $.ajax({ - type: 'POST', - url: $target.attr('href'), - dataType: 'json', - data: { - '_method': 'delete', - }, - success: (data) => { - $target.remove(); - $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>'); - this.updateBadges(data); - }, - }); - } - - updateState(target) { - const row = target.closest('li'); - const restoreBtn = row.querySelector('.js-undo-todo'); - const doneBtn = row.querySelector('.js-done-todo'); - - target.removeAttribute('disabled'); - target.classList.remove('disabled'); - target.classList.add('hidden'); - - if (target === doneBtn) { - row.classList.add('done-reversible'); - restoreBtn.classList.remove('hidden'); - } else { - row.classList.remove('done-reversible'); - doneBtn.classList.remove('hidden'); - } - } - - updateBadges(data) { - $(document).trigger('todo:toggle', data.count); - $('.todos-pending .badge').text(data.count); - $('.todos-done .badge').text(data.done_count); - } - - goToTodoUrl(e) { - const todoLink = this.dataset.url; - - if (!todoLink) { - return; - } - - if (gl.utils.isMetaClick(e)) { - const windowTarget = '_blank'; - const selected = e.target; - e.preventDefault(); - - if (selected.tagName === 'IMG') { - const avatarUrl = selected.parentElement.getAttribute('href'); - window.open(avatarUrl, windowTarget); - } else { - window.open(todoLink, windowTarget); - } - } else { - gl.utils.visitUrl(todoLink); - } - } - } - - global.Todos = Todos; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js index 500b78fc5d8..500b78fc5d8 100644 --- a/app/assets/javascripts/u2f/authenticate.js.es6 +++ b/app/assets/javascripts/u2f/authenticate.js diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js index 059e6c628b3..059e6c628b3 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js new file mode 100644 index 00000000000..99419e85b20 --- /dev/null +++ b/app/assets/javascripts/user_callout.js @@ -0,0 +1,60 @@ +/* global Cookies */ + +const userCalloutElementName = '.user-callout'; +const closeButton = '.close-user-callout'; +const userCalloutBtn = '.user-callout-btn'; +const userCalloutSvgAttrName = 'callout-svg'; + +const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; + +const USER_CALLOUT_TEMPLATE = ` + <div class="bordered-box landing content-block"> + <button class="btn btn-default close close-user-callout" type="button"> + <i class="fa fa-times dismiss-icon"></i> + </button> + <div class="row"> + <div class="col-sm-3 col-xs-12 svg-container"> + </div> + <div class="col-sm-8 col-xs-12 inner-content"> + <h4> + Customize your experience + </h4> + <p> + Change syntax themes, default project pages, and more in preferences. + </p> + <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a> + </div> + </div> +</div>`; + +class UserCallout { + constructor() { + this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); + this.userCalloutBody = $(userCalloutElementName); + this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); + $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); + this.init(); + } + + init() { + const $template = $(USER_CALLOUT_TEMPLATE); + if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { + $template.find('.svg-container').append(this.userCalloutSvg); + this.userCalloutBody.append($template); + $template.find(closeButton).on('click', e => this.dismissCallout(e)); + $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); + } else { + this.userCalloutBody.remove(); + } + } + + dismissCallout(e) { + Cookies.set(USER_CALLOUT_COOKIE, 'true'); + const $currentTarget = $(e.currentTarget); + if ($currentTarget.hasClass('close-user-callout')) { + this.userCalloutBody.remove(); + } + } +} + +module.exports = UserCallout; diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js index 465618e3d53..465618e3d53 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js index 137cefa3b8e..137cefa3b8e 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 5111b260e1c..754d448564f 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ -/* global d3 */ + +import d3 from 'd3'; (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 4cad60a59b1..580e2d84be5 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,3 +1 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); +require('./calendar'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index de33a31b411..eb897e9dfe9 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -53,13 +53,22 @@ $loading = $block.find('.block-loading').fadeOut(); var updateIssueBoardsIssue = function () { - $loading.fadeIn(); + $loading.removeClass('hidden').fadeIn(); gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) .then(function () { $loading.fadeOut(); }); }; + $('.assign-to-me-link').on('click', (e) => { + e.preventDefault(); + $(e.currentTarget).hide(); + const $input = $(`input[name="${$dropdown.data('field-name')}"]`); + $input.val(gon.current_user_id); + selectedId = $input.val(); + $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); + }); + $block.on('click', '.js-assign-yourself', function(e) { e.preventDefault(); @@ -81,7 +90,7 @@ data = {}; data[abilityName] = {}; data[abilityName].assignee_id = selected != null ? selected : null; - $loading.fadeIn(); + $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); return $.ajax({ type: 'PUT', @@ -199,15 +208,15 @@ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { e.preventDefault(); selectedId = user.id; + if (selectedId === gon.current_user_id) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } return; } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; - } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - selectedId = user.id; - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; - gl.issueBoards.BoardsStore.updateFiltersUrl(); - e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { selectedId = user.id; return Issuable.filterResults($dropdown.closest('form')); @@ -234,11 +243,16 @@ id: function (user) { return user.id; }, + opened: function(e) { + const $el = $(e.currentTarget); + $el.find('.is-active').removeClass('is-active'); + $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); + }, renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; username = user.username ? "@" + user.username : ""; avatar = user.avatar_url ? user.avatar_url : false; - selected = user.id === selectedId ? "is-active" : ""; + selected = user.id === parseInt(selectedId, 10) ? "is-active" : ""; img = ""; if (user.beforeDivider != null) { "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>"; @@ -248,7 +262,7 @@ } } // split into three parts so we can remove the username section if nessesary - listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; + listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; listClosingTags = "</a> </li>"; if (username === '') { diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js new file mode 100644 index 00000000000..d4f716acb72 --- /dev/null +++ b/app/assets/javascripts/version_check_image.js @@ -0,0 +1,10 @@ +class VersionCheckImage { + static bindErrorEvent(imageElement) { + imageElement.off('error').on('error', () => imageElement.hide()); + } +} + +window.gl = window.gl || {}; +gl.VersionCheckImage = VersionCheckImage; + +module.exports = VersionCheckImage; diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6 deleted file mode 100644 index 1fa2b5ac399..00000000000 --- a/app/assets/javascripts/version_check_image.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -(() => { - class VersionCheckImage { - static bindErrorEvent(imageElement) { - imageElement.off('error').on('error', () => imageElement.hide()); - } - } - - window.gl = window.gl || {}; - gl.VersionCheckImage = VersionCheckImage; -})(); diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js index f712d7ba930..f712d7ba930 100644 --- a/app/assets/javascripts/visibility_select.js.es6 +++ b/app/assets/javascripts/visibility_select.js diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js new file mode 100644 index 00000000000..aaebf29d8ae --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js @@ -0,0 +1,92 @@ +/* eslint-disable no-new, no-alert */ +/* global Flash */ +import '~/flash'; +import eventHub from '../event_hub'; + +export default { + props: { + endpoint: { + type: String, + required: true, + }, + + service: { + type: Object, + required: true, + }, + + title: { + type: String, + required: true, + }, + + icon: { + type: String, + required: true, + }, + + cssClass: { + type: String, + required: true, + }, + + confirmActionMessage: { + type: String, + required: false, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + computed: { + iconClass() { + return `fa fa-${this.icon}`; + }, + + buttonClass() { + return `btn has-tooltip ${this.cssClass}`; + }, + }, + + methods: { + onClick() { + if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { + this.makeRequest(); + } else if (!this.confirmActionMessage) { + this.makeRequest(); + } + }, + + makeRequest() { + this.isLoading = true; + + this.service.postAction(this.endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshPipelines'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + }, + + template: ` + <button + type="button" + @click="onClick" + :class="buttonClass" + :title="title" + :aria-label="title" + data-placement="top" + :disabled="isLoading"> + <i :class="iconClass" aria-hidden="true"/> + <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" /> + </button> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js new file mode 100644 index 00000000000..4e183d5c8ec --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js @@ -0,0 +1,56 @@ +export default { + props: [ + 'pipeline', + ], + computed: { + user() { + return !!this.pipeline.user; + }, + }, + template: ` + <td> + <a + :href="pipeline.path" + class="js-pipeline-url-link"> + <span class="pipeline-id">#{{pipeline.id}}</span> + </a> + <span>by</span> + <a + class="js-pipeline-url-user" + v-if="user" + :href="pipeline.user.web_url"> + <img + v-if="user" + class="avatar has-tooltip s20 " + :title="pipeline.user.name" + data-container="body" + :src="pipeline.user.avatar_url" + > + </a> + <span + v-if="!user" + class="js-pipeline-url-api api monospace"> + API + </span> + <span + v-if="pipeline.flags.latest" + class="js-pipeline-url-lastest label label-success has-tooltip" + title="Latest pipeline for this branch" + data-original-title="Latest pipeline for this branch"> + latest + </span> + <span + v-if="pipeline.flags.yaml_errors" + class="js-pipeline-url-yaml label label-danger has-tooltip" + :title="pipeline.yaml_errors" + :data-original-title="pipeline.yaml_errors"> + yaml invalid + </span> + <span + v-if="pipeline.flags.stuck" + class="js-pipeline-url-stuck label label-warning"> + stuck + </span> + </td> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js new file mode 100644 index 00000000000..4bb2b048884 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js @@ -0,0 +1,71 @@ +/* eslint-disable no-new */ +/* global Flash */ +import '~/flash'; +import playIconSvg from 'icons/_icon_play.svg'; +import eventHub from '../event_hub'; + +export default { + props: { + actions: { + type: Array, + required: true, + }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + playIconSvg, + isLoading: false, + }; + }, + + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshPipelines'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, + }, + + template: ` + <div class="btn-group" v-if="actions"> + <button + type="button" + class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + title="Manual job" + data-toggle="dropdown" + data-placement="top" + aria-label="Manual job" + :disabled="isLoading"> + ${playIconSvg} + <i class="fa fa-caret-down" aria-hidden="true"></i> + <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </button> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <button + type="button" + class="js-pipeline-action-link no-btn" + @click="onClickAction(action.path)"> + ${playIconSvg} + <span>{{action.name}}</span> + </button> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js new file mode 100644 index 00000000000..3555040d60f --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js @@ -0,0 +1,32 @@ +export default { + props: { + artifacts: { + type: Array, + required: true, + }, + }, + + template: ` + <div class="btn-group" role="group"> + <button + class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" + title="Artifacts" + data-placement="top" + data-toggle="dropdown" + aria-label="Artifacts"> + <i class="fa fa-download" aria-hidden="true"></i> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="artifact in artifacts"> + <a + rel="nofollow" + :href="artifact.path"> + <i class="fa fa-download" aria-hidden="true"></i> + <span>Download {{artifact.name}} artifacts</span> + </a> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js new file mode 100644 index 00000000000..a2c29002707 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js @@ -0,0 +1,116 @@ +/* global Flash */ +import canceledSvg from 'icons/_icon_status_canceled_borderless.svg'; +import createdSvg from 'icons/_icon_status_created_borderless.svg'; +import failedSvg from 'icons/_icon_status_failed_borderless.svg'; +import manualSvg from 'icons/_icon_status_manual_borderless.svg'; +import pendingSvg from 'icons/_icon_status_pending_borderless.svg'; +import runningSvg from 'icons/_icon_status_running_borderless.svg'; +import skippedSvg from 'icons/_icon_status_skipped_borderless.svg'; +import successSvg from 'icons/_icon_status_success_borderless.svg'; +import warningSvg from 'icons/_icon_status_warning_borderless.svg'; + +export default { + data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + svg: svgsDictionary[this.stage.status.icon], + }; + }, + + props: { + stage: { + type: Object, + required: true, + }, + }, + + updated() { + if (this.builds) { + this.stopDropdownClickPropagation(); + } + }, + + methods: { + fetchBuilds(e) { + const ariaExpanded = e.currentTarget.attributes['aria-expanded']; + + if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null; + + return this.$http.get(this.stage.dropdown_path) + .then((response) => { + this.builds = JSON.parse(response.body).html; + }, () => { + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { + e.stopPropagation(); + }); + }, + }, + computed: { + buildsOrSpinner() { + return this.builds ? this.builds : this.spinner; + }, + dropdownClass() { + if (this.builds) return 'js-builds-dropdown-container'; + return 'js-builds-dropdown-loading builds-dropdown-loading'; + }, + buildStatus() { + return `Build: ${this.stage.status.label}`; + }, + tooltip() { + return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; + }, + triggerButtonClass() { + return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; + }, + }, + template: ` + <div> + <button + @click="fetchBuilds($event)" + :class="triggerButtonClass" + :title="stage.title" + data-placement="top" + data-toggle="dropdown" + type="button" + :aria-label="stage.title"> + <span v-html="svg" aria-hidden="true"></span> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div class="arrow-up" aria-hidden="true"></div> + <div + :class="dropdownClass" + class="js-builds-dropdown-list scrollable-menu" + v-html="buildsOrSpinner"> + </div> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js new file mode 100644 index 00000000000..21a281af438 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/status.js @@ -0,0 +1,60 @@ +import canceledSvg from 'icons/_icon_status_canceled.svg'; +import createdSvg from 'icons/_icon_status_created.svg'; +import failedSvg from 'icons/_icon_status_failed.svg'; +import manualSvg from 'icons/_icon_status_manual.svg'; +import pendingSvg from 'icons/_icon_status_pending.svg'; +import runningSvg from 'icons/_icon_status_running.svg'; +import skippedSvg from 'icons/_icon_status_skipped.svg'; +import successSvg from 'icons/_icon_status_success.svg'; +import warningSvg from 'icons/_icon_status_warning.svg'; + +export default { + props: { + pipeline: { + type: Object, + required: true, + }, + }, + + data() { + const svgsDictionary = { + icon_status_canceled: canceledSvg, + icon_status_created: createdSvg, + icon_status_failed: failedSvg, + icon_status_manual: manualSvg, + icon_status_pending: pendingSvg, + icon_status_running: runningSvg, + icon_status_skipped: skippedSvg, + icon_status_success: successSvg, + icon_status_warning: warningSvg, + }; + + return { + svg: svgsDictionary[this.pipeline.details.status.icon], + }; + }, + + computed: { + cssClasses() { + return `ci-status ci-${this.pipeline.details.status.group}`; + }, + + detailsPath() { + const { status } = this.pipeline.details; + return status.has_details ? status.details_path : false; + }, + + content() { + return `${this.svg} ${this.pipeline.details.status.text}`; + }, + }, + template: ` + <td class="commit-link"> + <a + :class="cssClasses" + :href="detailsPath" + v-html="content"> + </a> + </td> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js new file mode 100644 index 00000000000..498d0715f54 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js @@ -0,0 +1,71 @@ +import iconTimerSvg from 'icons/_icon_timer.svg'; +import '../../lib/utils/datetime_utility'; + +export default { + data() { + return { + currentTime: new Date(), + iconTimerSvg, + }; + }, + props: ['pipeline'], + computed: { + timeAgo() { + return gl.utils.getTimeago(); + }, + localTimeFinished() { + return gl.utils.formatDate(this.pipeline.details.finished_at); + }, + timeStopped() { + const changeTime = this.currentTime; + const options = { + weekday: 'long', + year: 'numeric', + month: 'short', + day: 'numeric', + }; + options.timeZoneName = 'short'; + const finished = this.pipeline.details.finished_at; + if (!finished && changeTime) return false; + return ({ words: this.timeAgo.format(finished) }); + }, + duration() { + const { duration } = this.pipeline.details; + const date = new Date(duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + if (hh < 10) hh = `0${hh}`; + if (mm < 10) mm = `0${mm}`; + if (ss < 10) ss = `0${ss}`; + + if (duration !== null) return `${hh}:${mm}:${ss}`; + return false; + }, + }, + methods: { + changeTime() { + this.currentTime = new Date(); + }, + }, + template: ` + <td class="pipelines-time-ago"> + <p class="duration" v-if='duration'> + <span v-html="iconTimerSvg"></span> + {{duration}} + </p> + <p class="finished-at" v-if='timeStopped'> + <i class="fa fa-calendar"></i> + <time + data-toggle="tooltip" + data-placement="top" + data-container="body" + :data-original-title='localTimeFinished'> + {{timeStopped.words}} + </time> + </p> + </td> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js new file mode 100644 index 00000000000..b4e2d3a1143 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/index.js @@ -0,0 +1,28 @@ +import PipelinesStore from './stores/pipelines_store'; +import PipelinesComponent from './pipelines'; +import '../vue_shared/vue_resource_interceptor'; + +const Vue = window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); + +$(() => new Vue({ + el: document.querySelector('.vue-pipelines-index'), + + data() { + const project = document.querySelector('.pipelines'); + const store = new PipelinesStore(); + + return { + store, + endpoint: project.dataset.url, + }; + }, + components: { + 'vue-pipelines': PipelinesComponent, + }, + template: ` + <vue-pipelines + :endpoint="endpoint" + :store="store" /> + `, +})); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 deleted file mode 100644 index e7432afb56e..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue, VueResource, gl */ -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('../lib/utils/common_utils'); -require('../vue_shared/vue_resource_interceptor'); -require('./pipelines'); - -$(() => new Vue({ - el: document.querySelector('.vue-pipelines-index'), - - data() { - const project = document.querySelector('.pipelines'); - const svgs = document.querySelector('.pipeline-svgs').dataset; - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = gl.utils.DOMStringMapToObject(svgs); - - return { - scope: project.dataset.url, - store: new gl.PipelineStore(), - svgs: svgsObject, - }; - }, - components: { - 'vue-pipelines': gl.VuePipelines, - }, - template: ` - <vue-pipelines - :scope='scope' - :store='store' - :svgs='svgs' - > - </vue-pipelines> - `, -})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 deleted file mode 100644 index 54e8f977a47..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -/* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VuePipelineActions = Vue.extend({ - props: ['pipeline', 'svgs'], - computed: { - actions() { - return this.pipeline.details.manual_actions.length > 0; - }, - artifacts() { - return this.pipeline.details.artifacts.length > 0; - }, - }, - methods: { - download(name) { - return `Download ${name} artifacts`; - }, - }, - template: ` - <td class="pipeline-actions hidden-xs"> - <div class="controls pull-right"> - <div class="btn-group inline"> - <div class="btn-group"> - <button - v-if='actions' - class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" - data-toggle="dropdown" - title="Manual job" - data-placement="top" - aria-label="Manual job" - > - <span v-html='svgs.iconPlay' aria-hidden="true"></span> - <i class="fa fa-caret-down" aria-hidden="true"></i> - </button> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for='action in pipeline.details.manual_actions'> - <a - rel="nofollow" - data-method="post" - :href='action.path' - > - <span v-html='svgs.iconPlay' aria-hidden="true"></span> - <span>{{action.name}}</span> - </a> - </li> - </ul> - </div> - <div class="btn-group"> - <button - v-if='artifacts' - class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - title="Artifacts" - data-placement="top" - data-toggle="dropdown" - aria-label="Artifacts" - > - <i class="fa fa-download" aria-hidden="true"></i> - <i class="fa fa-caret-down" aria-hidden="true"></i> - </button> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for='artifact in pipeline.details.artifacts'> - <a - rel="nofollow" - download - :href='artifact.path' - > - <i class="fa fa-download" aria-hidden="true"></i> - <span>{{download(artifact.name)}}</span> - </a> - </li> - </ul> - </div> - </div> - <div class="cancel-retry-btns inline"> - <a - v-if='pipeline.flags.retryable' - class="btn has-tooltip" - title="Retry" - rel="nofollow" - data-method="post" - data-placement="top" - data-toggle="dropdown" - :href='pipeline.retry_path' - aria-label="Retry"> - <i class="fa fa-repeat" aria-hidden="true"></i> - </a> - <a - v-if='pipeline.flags.cancelable' - class="btn btn-remove has-tooltip" - title="Cancel" - rel="nofollow" - data-method="post" - data-placement="top" - data-toggle="dropdown" - :href='pipeline.cancel_path' - aria-label="Cancel"> - <i class="fa fa-remove" aria-hidden="true"></i> - </a> - </div> - </div> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 deleted file mode 100644 index ae5649f0519..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 +++ /dev/null @@ -1,63 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VuePipelineUrl = Vue.extend({ - props: [ - 'pipeline', - ], - computed: { - user() { - return !!this.pipeline.user; - }, - }, - template: ` - <td> - <a :href='pipeline.path'> - <span class="pipeline-id">#{{pipeline.id}}</span> - </a> - <span>by</span> - <a - v-if='user' - :href='pipeline.user.web_url' - > - <img - v-if='user' - class="avatar has-tooltip s20 " - :title='pipeline.user.name' - data-container="body" - :src='pipeline.user.avatar_url' - > - </a> - <span - v-if='!user' - class="api monospace" - > - API - </span> - <span - v-if='pipeline.flags.latest' - class="label label-success has-tooltip" - title="Latest pipeline for this branch" - data-original-title="Latest pipeline for this branch" - > - latest - </span> - <span - v-if='pipeline.flags.yaml_errors' - class="label label-danger has-tooltip" - :title='pipeline.yaml_errors' - :data-original-title='pipeline.yaml_errors' - > - yaml invalid - </span> - <span - v-if='pipeline.flags.stuck' - class="label label-warning" - > - stuck - </span> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js new file mode 100644 index 00000000000..f389e5e4950 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -0,0 +1,121 @@ +/* global Flash */ +/* eslint-disable no-new */ +import '~/flash'; +import Vue from 'vue'; +import PipelinesService from './services/pipelines_service'; +import eventHub from './event_hub'; +import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; +import TablePaginationComponent from '../vue_shared/components/table_pagination'; + +export default { + props: { + endpoint: { + type: String, + required: true, + }, + + store: { + type: Object, + required: true, + }, + }, + + components: { + 'gl-pagination': TablePaginationComponent, + 'pipelines-table-component': PipelinesTableComponent, + }, + + data() { + return { + state: this.store.state, + apiScope: 'all', + pagenum: 1, + pageRequest: false, + }; + }, + + created() { + this.service = new PipelinesService(this.endpoint); + + this.fetchPipelines(); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + }, + + beforeUpdate() { + if (this.state.pipelines.length && this.$children) { + this.store.startTimeAgoLoops.call(this, Vue); + } + }, + + beforeDestroyed() { + eventHub.$off('refreshPipelines'); + }, + + methods: { + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + */ + change(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + + fetchPipelines() { + const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; + const scope = gl.utils.getParameterByName('scope') || this.apiScope; + + this.pageRequest = true; + return this.service.getPipelines(scope, pageNumber) + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeCount(response.body.count); + this.store.storePipelines(response.body.pipelines); + this.store.storePagination(response.headers); + }) + .then(() => { + this.pageRequest = false; + }) + .catch(() => { + this.pageRequest = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.'); + }); + }, + }, + template: ` + <div> + <div class="pipelines realtime-loading" v-if="pageRequest"> + <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!pageRequest && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder" v-if="!pageRequest && state.pipelines.length"> + <pipelines-table-component + :pipelines="state.pipelines" + :service="service"/> + </div> + + <gl-pagination + v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage" + :pagenum="pagenum" + :change="change" + :count="state.count.all" + :pageInfo="state.pageInfo" + > + </gl-pagination> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 deleted file mode 100644 index 0265c00a414..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ /dev/null @@ -1,93 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -window.Vue = require('vue'); -require('../vue_shared/components/table_pagination'); -require('./store'); -require('../vue_shared/components/pipelines_table'); -const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store'); - -((gl) => { - gl.VuePipelines = Vue.extend({ - - components: { - 'gl-pagination': gl.VueGlPagination, - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - data() { - return { - pipelines: [], - timeLoopInterval: '', - intervalId: '', - apiScope: 'all', - pageInfo: {}, - pagenum: 1, - count: { all: 0, running_or_pending: 0 }, - pageRequest: false, - }; - }, - props: ['scope', 'store', 'svgs'], - created() { - const pagenum = gl.utils.getParameterByName('page'); - const scope = gl.utils.getParameterByName('scope'); - if (pagenum) this.pagenum = pagenum; - if (scope) this.apiScope = scope; - - this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); - }, - - beforeUpdate() { - if (this.pipelines.length && this.$children) { - CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue); - } - }, - - methods: { - /** - * Changes the URL according to the pagination component. - * - * If no scope is provided, 'all' is assumed. - * - * Pagination component sends "null" when no scope is provided. - * - * @param {Number} pagenum - * @param {String} apiScope = 'all' - */ - change(pagenum, apiScope) { - if (!apiScope) apiScope = 'all'; - gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`); - }, - }, - template: ` - <div> - <div class="pipelines realtime-loading" v-if='pageRequest'> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!pageRequest && pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> - </div> - - <div class="table-holder" v-if='!pageRequest && pipelines.length'> - <pipelines-table-component - :pipelines='pipelines' - :svgs='svgs'> - </pipelines-table-component> - </div> - - <gl-pagination - v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage' - :pagenum='pagenum' - :change='change' - :count='count.all' - :pageInfo='pageInfo' - > - </gl-pagination> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js new file mode 100644 index 00000000000..708f5068dd3 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js @@ -0,0 +1,44 @@ +/* eslint-disable class-methods-use-this */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class PipelinesService { + + /** + * Commits and merge request endpoints need to be requested with `.json`. + * + * The url provided to request the pipelines in the new merge request + * page already has `.json`. + * + * @param {String} root + */ + constructor(root) { + let endpoint; + + if (root.indexOf('.json') === -1) { + endpoint = `${root}.json`; + } else { + endpoint = root; + } + + this.pipelines = Vue.resource(endpoint); + } + + getPipelines(scope, page) { + return this.pipelines.get({ scope, page }); + } + + /** + * Post request for all pipelines actions. + * Endpoint content type needs to be: + * `Content-Type:application/x-www-form-urlencoded` + * + * @param {String} endpoint + * @return {Promise} + */ + postAction(endpoint) { + return Vue.http.post(endpoint, {}, { emulateJSON: true }); + } +} diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 deleted file mode 100644 index 8cc417a9966..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ /dev/null @@ -1,103 +0,0 @@ -/* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStage = Vue.extend({ - data() { - return { - builds: '', - spinner: '<span class="fa fa-spinner fa-spin"></span>', - }; - }, - props: { - stage: { - type: Object, - required: true, - }, - svgs: { - type: Object, - required: true, - }, - match: { - type: Function, - required: true, - }, - }, - methods: { - fetchBuilds(e) { - const areaExpanded = e.currentTarget.attributes['aria-expanded']; - - if (areaExpanded && (areaExpanded.textContent === 'true')) return null; - - return this.$http.get(this.stage.dropdown_path) - .then((response) => { - this.builds = JSON.parse(response.body).html; - }, () => { - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); - }, - keepGraph(e) { - const { target } = e; - - if (target.className.indexOf('js-ci-action-icon') >= 0) return null; - - if ( - target.parentElement && - (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) - ) return null; - - return e.stopPropagation(); - }, - }, - computed: { - buildsOrSpinner() { - return this.builds ? this.builds : this.spinner; - }, - dropdownClass() { - if (this.builds) return 'js-builds-dropdown-container'; - return 'js-builds-dropdown-loading builds-dropdown-loading'; - }, - buildStatus() { - return `Build: ${this.stage.status.label}`; - }, - tooltip() { - return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; - }, - svg() { - const { icon } = this.stage.status; - const stageIcon = icon.replace(/icon/i, 'stage_icon'); - return this.svgs[this.match(stageIcon)]; - }, - triggerButtonClass() { - return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; - }, - }, - template: ` - <div> - <button - @click='fetchBuilds($event)' - :class="triggerButtonClass" - :title='stage.title' - data-placement="top" - data-toggle="dropdown" - type="button" - :aria-label='stage.title' - > - <span v-html="svg" aria-hidden="true"></span> - <i class="fa fa-caret-down" aria-hidden="true"></i> - </button> - <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> - <div class="arrow-up" aria-hidden="true"></div> - <div - @click='keepGraph($event)' - :class="dropdownClass" - class="js-builds-dropdown-list scrollable-menu" - v-html="buildsOrSpinner" - > - </div> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 deleted file mode 100644 index 05175082704..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/status.js.es6 +++ /dev/null @@ -1,34 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStatusScope = Vue.extend({ - props: [ - 'pipeline', 'svgs', 'match', - ], - computed: { - cssClasses() { - const cssObject = { 'ci-status': true }; - cssObject[`ci-${this.pipeline.details.status.group}`] = true; - return cssObject; - }, - svg() { - return this.svgs[this.match(this.pipeline.details.status.icon)]; - }, - detailsPath() { - const { status } = this.pipeline.details; - return status.has_details ? status.details_path : false; - }, - }, - template: ` - <td class="commit-link"> - <a - :class='cssClasses' - :href='detailsPath' - v-html='svg + pipeline.details.status.text' - > - </a> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 deleted file mode 100644 index 909007267b9..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -/* global gl, Flash */ -/* eslint-disable no-param-reassign */ - -((gl) => { - const pageValues = (headers) => { - const normalized = gl.utils.normalizeHeaders(headers); - const paginationInfo = gl.utils.parseIntPagination(normalized); - return paginationInfo; - }; - - gl.PipelineStore = class { - fetchDataLoop(Vue, pageNum, url, apiScope) { - this.pageRequest = true; - - return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) - .then((response) => { - const pageInfo = pageValues(response.headers); - this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); - - const res = JSON.parse(response.body); - this.count = Object.assign({}, this.count, res.count); - this.pipelines = Object.assign([], this.pipelines, res.pipelines); - - this.pageRequest = false; - }, () => { - this.pageRequest = false; - return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); - }); - } - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js new file mode 100644 index 00000000000..7ac10086a55 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js @@ -0,0 +1,61 @@ +/* eslint-disable no-underscore-dangle*/ +import '../../vue_realtime_listener'; + +export default class PipelinesStore { + constructor() { + this.state = {}; + + this.state.pipelines = []; + this.state.count = {}; + this.state.pageInfo = {}; + } + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + } + + storeCount(count = {}) { + this.state.count = count; + } + + storePagination(pagination = {}) { + let paginationInfo; + + if (Object.keys(pagination).length) { + const normalizedHeaders = gl.utils.normalizeHeaders(pagination); + paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + } else { + paginationInfo = pagination; + } + + this.state.pageInfo = paginationInfo; + } + + /** + * FIXME: Move this inside the component. + * + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } +} diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 deleted file mode 100644 index 3598da11573..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -window.Vue = require('vue'); -require('../lib/utils/datetime_utility'); - -((gl) => { - gl.VueTimeAgo = Vue.extend({ - data() { - return { - currentTime: new Date(), - }; - }, - props: ['pipeline', 'svgs'], - computed: { - timeAgo() { - return gl.utils.getTimeago(); - }, - localTimeFinished() { - return gl.utils.formatDate(this.pipeline.details.finished_at); - }, - timeStopped() { - const changeTime = this.currentTime; - const options = { - weekday: 'long', - year: 'numeric', - month: 'short', - day: 'numeric', - }; - options.timeZoneName = 'short'; - const finished = this.pipeline.details.finished_at; - if (!finished && changeTime) return false; - return ({ words: this.timeAgo.format(finished) }); - }, - duration() { - const { duration } = this.pipeline.details; - const date = new Date(duration * 1000); - - let hh = date.getUTCHours(); - let mm = date.getUTCMinutes(); - let ss = date.getSeconds(); - - if (hh < 10) hh = `0${hh}`; - if (mm < 10) mm = `0${mm}`; - if (ss < 10) ss = `0${ss}`; - - if (duration !== null) return `${hh}:${mm}:${ss}`; - return false; - }, - }, - methods: { - changeTime() { - this.currentTime = new Date(); - }, - }, - template: ` - <td> - <p class="duration" v-if='duration'> - <span v-html='svgs.iconTimer'></span> - {{duration}} - </p> - <p class="finished-at" v-if='timeStopped'> - <i class="fa fa-calendar"></i> - <time - data-toggle="tooltip" - data-placement="top" - data-container="body" - :data-original-title='localTimeFinished' - > - {{timeStopped.words}} - </time> - </p> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js index 30f6680a673..30f6680a673 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js.es6 +++ b/app/assets/javascripts/vue_realtime_listener/index.js diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js new file mode 100644 index 00000000000..fb68abd95a2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/commit.js @@ -0,0 +1,157 @@ +import commitIconSvg from 'icons/_icon_commit.svg'; + +export default { + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + }, + + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + }, + + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.web_url && + this.author.username; + }, + + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + + data() { + return { commitIconSvg }; + }, + + template: ` + <div class="branch-commit"> + + <div v-if="hasCommitRef" class="icon-container"> + <i v-if="tag" class="fa fa-tag"></i> + <i v-if="!tag" class="fa fa-code-fork"></i> + </div> + + <a v-if="hasCommitRef" + class="monospace branch-name" + :href="commitRef.ref_url"> + {{commitRef.name}} + </a> + + <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> + + <a class="commit-id monospace" + :href="commitUrl"> + {{shortSha}} + </a> + + <p class="commit-title"> + <span v-if="title"> + <a v-if="hasAuthor" + class="avatar-image-container" + :href="author.web_url"> + <img + class="avatar has-tooltip s20" + :src="author.avatar_url" + :alt="userImageAltDescription" + :title="author.username" /> + </a> + + <a class="commit-row-message" + :href="commitUrl"> + {{title}} + </a> + </span> + <span v-else> + Cant find HEAD commit for this branch + </span> + </p> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 deleted file mode 100644 index ff88e236829..00000000000 --- a/app/assets/javascripts/vue_shared/components/commit.js.es6 +++ /dev/null @@ -1,164 +0,0 @@ -/* global Vue */ -window.Vue = require('vue'); - -(() => { - window.gl = window.gl || {}; - - window.gl.CommitComponent = Vue.component('commit-component', { - - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render `fa-code-fork` icon. - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - - commitIconSvg: { - type: String, - required: false, - }, - }, - - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.web_url && - this.author.username; - }, - - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, - }, - - template: ` - <div class="branch-commit"> - - <div v-if="hasCommitRef" class="icon-container"> - <i v-if="tag" class="fa fa-tag"></i> - <i v-if="!tag" class="fa fa-code-fork"></i> - </div> - - <a v-if="hasCommitRef" - class="monospace branch-name" - :href="commitRef.ref_url"> - {{commitRef.name}} - </a> - - <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> - - <a class="commit-id monospace" - :href="commitUrl"> - {{shortSha}} - </a> - - <p class="commit-title"> - <span v-if="title"> - <a v-if="hasAuthor" - class="avatar-image-container" - :href="author.web_url"> - <img - class="avatar has-tooltip s20" - :src="author.avatar_url" - :alt="userImageAltDescription" - :title="author.username" /> - </a> - - <a class="commit-row-message" - :href="commitUrl"> - {{title}} - </a> - </span> - <span v-else> - Cant find HEAD commit for this branch - </span> - </p> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js new file mode 100644 index 00000000000..afd8d7acf6b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js @@ -0,0 +1,48 @@ +import PipelinesTableRowComponent from './pipelines_table_row'; + +/** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ +export default { + props: { + pipelines: { + type: Array, + required: true, + default: () => ([]), + }, + + service: { + type: Object, + required: true, + }, + }, + + components: { + 'pipelines-table-row-component': PipelinesTableRowComponent, + }, + + template: ` + <table class="table ci-table"> + <thead> + <tr> + <th class="js-pipeline-status pipeline-status">Status</th> + <th class="js-pipeline-info pipeline-info">Pipeline</th> + <th class="js-pipeline-commit pipeline-commit">Commit</th> + <th class="js-pipeline-stages pipeline-stages">Stages</th> + <th class="js-pipeline-date pipeline-date"></th> + <th class="js-pipeline-actions pipeline-actions"></th> + </tr> + </thead> + <tbody> + <template v-for="model in pipelines" + v-bind:model="model"> + <tr is="pipelines-table-row-component" + :pipeline="model" + :service="service"></tr> + </template> + </tbody> + </table> + `, +}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 deleted file mode 100644 index 4bdaef31ee9..00000000000 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -require('./pipelines_table_row'); -/** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ - -(() => { - window.gl = window.gl || {}; - gl.pipelines = gl.pipelines || {}; - - gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { - - props: { - pipelines: { - type: Array, - required: true, - default: () => ([]), - }, - - /** - * TODO: Remove this when we have webpack. - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, - }, - - components: { - 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, - }, - - template: ` - <table class="table ci-table"> - <thead> - <tr> - <th class="js-pipeline-status pipeline-status">Status</th> - <th class="js-pipeline-info pipeline-info">Pipeline</th> - <th class="js-pipeline-commit pipeline-commit">Commit</th> - <th class="js-pipeline-stages pipeline-stages">Stages</th> - <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> - </tr> - </thead> - <tbody> - <template v-for="model in pipelines" - v-bind:model="model"> - <tr is="pipelines-table-row-component" - :pipeline="model" - :svgs="svgs"></tr> - </template> - </tbody> - </table> - `, - }); -})(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js new file mode 100644 index 00000000000..f5b3cb9214e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js @@ -0,0 +1,228 @@ +/* eslint-disable no-param-reassign */ + +import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button'; +import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions'; +import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts'; +import PipelinesStatusComponent from '../../vue_pipelines_index/components/status'; +import PipelinesStageComponent from '../../vue_pipelines_index/components/stage'; +import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url'; +import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago'; +import CommitComponent from './commit'; + +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ +export default { + props: { + pipeline: { + type: Object, + required: true, + }, + + service: { + type: Object, + required: true, + }, + }, + + components: { + 'async-button-component': AsyncButtonComponent, + 'pipelines-actions-component': PipelinesActionsComponent, + 'pipelines-artifacts-component': PipelinesArtifactsComponent, + 'commit-component': CommitComponent, + 'dropdown-stage': PipelinesStageComponent, + 'pipeline-url': PipelinesUrlComponent, + 'status-scope': PipelinesStatusComponent, + 'time-ago': PipelinesTimeagoComponent, + }, + + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code + * + * @returns {Object|Undefined} + */ + commitAuthor() { + let commitAuthorInformation; + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + } + + // 4. If committer is not a GitLab User he/she can have a Gravatar + if (this.pipeline && + this.pipeline.commit) { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + web_url: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + + return commitAuthorInformation; + }, + + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matches `path` prop sent in the API to `ref_url` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'path') { + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + }, + + template: ` + <tr class="commit"> + <status-scope :pipeline="pipeline"/> + + <pipeline-url :pipeline="pipeline"></pipeline-url> + + <td> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor"/> + </td> + + <td class="stage-cell"> + <div class="stage-container dropdown js-mini-pipeline-graph" + v-if="pipeline.details.stages.length > 0" + v-for="stage in pipeline.details.stages"> + <dropdown-stage :stage="stage"/> + </div> + </td> + + <time-ago :pipeline="pipeline"/> + + <td class="pipeline-actions"> + <div class="pull-right btn-group"> + <pipelines-actions-component + v-if="pipeline.details.manual_actions.length" + :actions="pipeline.details.manual_actions" + :service="service" /> + + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + :artifacts="pipeline.details.artifacts" /> + + <async-button-component + v-if="pipeline.flags.retryable" + :service="service" + :endpoint="pipeline.retry_path" + css-class="js-pipelines-retry-button btn-default btn-retry" + title="Retry" + icon="repeat" /> + + <async-button-component + v-if="pipeline.flags.cancelable" + :service="service" + :endpoint="pipeline.cancel_path" + css-class="js-pipelines-cancel-button btn-remove" + title="Cancel" + icon="remove" + confirm-action-message="Are you sure you want to cancel this pipeline?" /> + </div> + </td> + </tr> + `, +}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 deleted file mode 100644 index 61c1b72d9d2..00000000000 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ /dev/null @@ -1,234 +0,0 @@ -/* eslint-disable no-param-reassign */ -/* global Vue */ - -require('../../vue_pipelines_index/status'); -require('../../vue_pipelines_index/pipeline_url'); -require('../../vue_pipelines_index/stage'); -require('../../vue_pipelines_index/pipeline_actions'); -require('../../vue_pipelines_index/time_ago'); -require('./commit'); -/** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ -(() => { - window.gl = window.gl || {}; - gl.pipelines = gl.pipelines || {}; - - gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', { - - props: { - pipeline: { - type: Object, - required: true, - default: () => ({}), - }, - - /** - * TODO: Remove this when we have webpack; - */ - svgs: { - type: Object, - required: true, - default: () => ({}), - }, - }, - - components: { - 'commit-component': gl.CommitComponent, - 'pipeline-actions': gl.VuePipelineActions, - 'dropdown-stage': gl.VueStage, - 'pipeline-url': gl.VuePipelineUrl, - 'status-scope': gl.VueStatusScope, - 'time-ago': gl.VueTimeAgo, - }, - - computed: { - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * This field needs a lot of verification, because of different possible cases: - * - * 1. person who is an author of a commit might be a GitLab user - * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar - * 3. If GitLab user does not have avatar he/she might have a Gravatar - * 4. If committer is not a GitLab User he/she can have a Gravatar - * 5. We do not have consistent API object in this case - * 6. We should improve API and the code - * - * @returns {Object|Undefined} - */ - commitAuthor() { - let commitAuthorInformation; - - // 1. person who is an author of a commit might be a GitLab user - if (this.pipeline && - this.pipeline.commit && - this.pipeline.commit.author) { - // 2. if person who is an author of a commit is a GitLab user - // he/she can have a GitLab avatar - if (this.pipeline.commit.author.avatar_url) { - commitAuthorInformation = this.pipeline.commit.author; - - // 3. If GitLab user does not have avatar he/she might have a Gravatar - } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { - avatar_url: this.pipeline.commit.author_gravatar_url, - }); - } - } - - // 4. If committer is not a GitLab User he/she can have a Gravatar - if (this.pipeline && - this.pipeline.commit) { - commitAuthorInformation = { - avatar_url: this.pipeline.commit.author_gravatar_url, - web_url: `mailto:${this.pipeline.commit.author_email}`, - username: this.pipeline.commit.author_name, - }; - } - - return commitAuthorInformation; - }, - - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.pipeline.ref && - this.pipeline.ref.tag) { - return this.pipeline.ref.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * Needed to render the commit component column. - * - * Matches `path` prop sent in the API to `ref_url` prop needed - * in the commit component. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'path') { - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } - - return undefined; - }, - - /** - * If provided, returns the commit url. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, - }, - - methods: { - /** - * FIXME: This should not be in this component but in the components that - * need this function. - * - * Used to render SVGs in the following components: - * - status-scope - * - dropdown-stage - * - * @param {String} string - * @return {String} - */ - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, - }, - - template: ` - <tr class="commit"> - <status-scope - :pipeline="pipeline" - :svgs="svgs" - :match="match"> - </status-scope> - - <pipeline-url :pipeline="pipeline"></pipeline-url> - - <td> - <commit-component - :tag="commitTag" - :commit-ref="commitRef" - :commit-url="commitUrl" - :short-sha="commitShortSha" - :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="svgs.commitIconSvg"> - </commit-component> - </td> - - <td class="stage-cell"> - <div class="stage-container dropdown js-mini-pipeline-graph" - v-if="pipeline.details.stages.length > 0" - v-for="stage in pipeline.details.stages"> - <dropdown-stage - :stage="stage" - :svgs="svgs" - :match="match"> - </dropdown-stage> - </div> - </td> - - <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago> - - <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions> - </tr> - `, - }); -})(); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js new file mode 100644 index 00000000000..b9cd28f6249 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js @@ -0,0 +1,135 @@ +const PAGINATION_UI_BUTTON_LIMIT = 4; +const UI_LIMIT = 6; +const SPREAD = '...'; +const PREV = 'Prev'; +const NEXT = 'Next'; +const FIRST = '<< First'; +const LAST = 'Last >>'; + +export default { + props: { + /** + This function will take the information given by the pagination component + + Here is an example `change` method: + + change(pagenum) { + gl.utils.visitUrl(`?page=${pagenum}`); + }, + */ + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages); + break; + case NEXT: + this.change(nextPage); + break; + case PREV: + this.change(previousPage); + break; + case FIRST: + this.change(1); + break; + default: + this.change(+text); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i += 1) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` + <div class="gl-pagination"> + <ul class="pagination clearfix"> + <li v-for='item in getItems' + :class='{ + page: item.page, + prev: item.prev, + next: item.next, + separator: item.separator, + active: item.active, + disabled: item.disabled + }' + > + <a @click="changePage($event)">{{item.title}}</a> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 deleted file mode 100644 index d8042a9b7fc..00000000000 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 +++ /dev/null @@ -1,150 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign, no-plusplus */ - -window.Vue = require('vue'); - -((gl) => { - const PAGINATION_UI_BUTTON_LIMIT = 4; - const UI_LIMIT = 6; - const SPREAD = '...'; - const PREV = 'Prev'; - const NEXT = 'Next'; - const FIRST = '<< First'; - const LAST = 'Last >>'; - - gl.VueGlPagination = Vue.extend({ - props: { - - // TODO: Consider refactoring in light of turbolinks removal. - - /** - This function will take the information given by the pagination component - And make a new Turbolinks call - - Here is an example `change` method: - - change(pagenum, apiScope) { - gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); - }, - */ - - change: { - type: Function, - required: true, - }, - - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - const apiScope = gl.utils.getParameterByName('scope'); - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages, apiScope); - break; - case NEXT: - this.change(nextPage, apiScope); - break; - case PREV: - this.change(previousPage, apiScope); - break; - case FIRST: - this.change(1, apiScope); - break; - default: - this.change(+text, apiScope); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; - }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) items.push({ title: FIRST }); - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i++) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) items.push({ title: LAST, last: true }); - - return items; - }, - }, - template: ` - <div class="gl-pagination"> - <ul class="pagination clearfix"> - <li v-for='item in getItems' - :class='{ - page: item.page, - prev: item.prev, - next: item.next, - separator: item.separator, - active: item.active, - disabled: item.disabled - }' - > - <a @click="changePage($event)">{{item.title}}</a> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js new file mode 100644 index 00000000000..f1c1e553b16 --- /dev/null +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign, no-plusplus */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next(() => { + Vue.activeResources--; + }); +}); + +Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); +}); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 deleted file mode 100644 index d3229f9f730..00000000000 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, -no-param-reassign, no-plusplus */ -/* global Vue */ - -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); - } - - Vue.activeResources--; - }); -}); - -Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); -}); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js index 75fd1394a03..75fd1394a03 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 1dcd1f8a6fc..83a8eeaafde 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,7 +2,6 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 39cf3b5f8ae..5bb7e8caec1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -44,5 +44,6 @@ @import "framework/images.scss"; @import "framework/broadcast-messages"; @import "framework/emojis.scss"; +@import "framework/emoji-sprites.scss"; @import "framework/icons.scss"; @import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 49907417e26..546718ddaf8 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -7,6 +7,7 @@ .emoji-menu { position: absolute; + top: 0; margin-top: 3px; padding: $gl-padding; z-index: 9; @@ -20,7 +21,7 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.67,.06,.19,1.44); transition-property: transform, opacity; &.is-aligned-right { @@ -47,12 +48,13 @@ } .emoji-menu-list { - list-style: none; - padding-left: 0; margin-bottom: 0; + padding-left: 0; + list-style: none; } .emoji-menu-list-item { + float: left; padding: 3px; margin-left: 1px; margin-right: 1px; @@ -94,7 +96,7 @@ .award-control { margin: 3px 5px 3px 0; - padding: 5px 6px; + padding: .35em .4em; outline: 0; &.disabled { @@ -136,10 +138,12 @@ } .icon, + gl-emoji, .award-control-icon { - float: left; - margin-right: 5px; - font-size: 18px; + vertical-align: middle; + margin-right: 0.15em; + font-size: 1.5em; + line-height: 1; } .award-control-icon-loading { @@ -150,4 +154,8 @@ color: $border-gray-normal; margin-top: 1px; } + + .award-control-text { + vertical-align: middle; + } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 0f9213b98e3..9a4129cdc8d 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -229,7 +229,7 @@ .controls { float: right; margin-top: 8px; - padding-bottom: 7px; + padding-bottom: 8px; border-bottom: 1px solid $border-color; } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index fb8ea18d122..9a0f7a14e57 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,6 +1,7 @@ .calender-block { padding-left: 0; padding-right: 0; + border-top: 0; direction: rtl; @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index a4b38723bbd..2c33b235980 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -429,3 +429,9 @@ table { @include str-truncated(100%); } } + +.tooltip { + .tooltip-inner { + word-wrap: break-word; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ff31e7f7b3d..186bb9ac616 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -43,7 +43,7 @@ white-space: nowrap; &[disabled] { - background-color: $input-bg-disabled; + opacity: .65; cursor: not-allowed; } @@ -96,7 +96,7 @@ .dropdown-menu-toggle { @extend .dropdown-toggle; - padding-right: 20px; + padding-right: 25px; position: relative; width: 163px; text-overflow: ellipsis; @@ -107,11 +107,12 @@ &.fa-spinner { font-size: 16px; - margin-top: -8px; + margin-top: -3px; } } - .fa-chevron-down { + .fa-chevron-down, + .fa-spinner { position: absolute; top: 11px; right: 8px; @@ -158,12 +159,12 @@ li { text-align: left; list-style: none; - padding: 0 8px; + padding: 0 10px; } .divider { height: 1px; - margin: 8px; + margin: 6px 10px; padding: 0; background-color: $dropdown-divider-color; } @@ -180,7 +181,7 @@ display: block; position: relative; padding: 5px 8px; - color: $dropdown-link-color; + color: $gl-text-color; line-height: initial; text-overflow: ellipsis; border-radius: 2px; @@ -192,6 +193,10 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; + + .badge { + background-color: darken($row-hover, 5%); + } } &.dropdown-menu-empty-link { @@ -213,10 +218,12 @@ } .dropdown-header { - color: $gl-text-color-secondary; + color: $gl-text-color; font-size: 13px; + font-weight: 600; line-height: 22px; - padding: 0 10px; + text-transform: capitalize; + padding: 0 16px; } .separator + .dropdown-header { @@ -228,6 +235,12 @@ padding: 5px 8px; color: $gl-text-color-secondary; } + + .badge { + position: absolute; + right: 8px; + top: 5px; + } } .dropdown-menu-drop-up { @@ -313,14 +326,17 @@ .dropdown-menu-selectable { a { - padding-left: 25px; + padding-left: 26px; &.is-indeterminate, &.is-active { + font-weight: 600; + color: $gl-text-color; + &::before { position: absolute; - left: 5px; - top: 8px; + left: 6px; + top: 6px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -342,7 +358,7 @@ .dropdown-title { position: relative; - padding: 0 25px 10px; + padding: 2px 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -372,7 +388,7 @@ right: 5px; width: 20px; height: 20px; - top: -3px; + top: -1px; } .dropdown-menu-back { diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss new file mode 100644 index 00000000000..925415f84b1 --- /dev/null +++ b/app/assets/stylesheets/framework/emoji-sprites.scss @@ -0,0 +1,1811 @@ +.emoji-zzz { background-position: 0 0; } +.emoji-1234 { background-position: -20px 0; } +.emoji-1F627 { background-position: 0 -20px; } +.emoji-8ball { background-position: -20px -20px; } +.emoji-a { background-position: -40px 0; } +.emoji-ab { background-position: -40px -20px; } +.emoji-abc { background-position: 0 -40px; } +.emoji-abcd { background-position: -20px -40px; } +.emoji-accept { background-position: -40px -40px; } +.emoji-aerial_tramway { background-position: -60px 0; } +.emoji-airplane { background-position: -60px -20px; } +.emoji-airplane_arriving { background-position: -60px -40px; } +.emoji-airplane_departure { background-position: 0 -60px; } +.emoji-airplane_small { background-position: -20px -60px; } +.emoji-alarm_clock { background-position: -40px -60px; } +.emoji-alembic { background-position: -60px -60px; } +.emoji-alien { background-position: -80px 0; } +.emoji-ambulance { background-position: -80px -20px; } +.emoji-amphora { background-position: -80px -40px; } +.emoji-anchor { background-position: -80px -60px; } +.emoji-angel { background-position: 0 -80px; } +.emoji-angel_tone1 { background-position: -20px -80px; } +.emoji-angel_tone2 { background-position: -40px -80px; } +.emoji-angel_tone3 { background-position: -60px -80px; } +.emoji-angel_tone4 { background-position: -80px -80px; } +.emoji-angel_tone5 { background-position: -100px 0; } +.emoji-anger { background-position: -100px -20px; } +.emoji-anger_right { background-position: -100px -40px; } +.emoji-angry { background-position: -100px -60px; } +.emoji-ant { background-position: -100px -80px; } +.emoji-apple { background-position: 0 -100px; } +.emoji-aquarius { background-position: -20px -100px; } +.emoji-aries { background-position: -40px -100px; } +.emoji-arrow_backward { background-position: -60px -100px; } +.emoji-arrow_double_down { background-position: -80px -100px; } +.emoji-arrow_double_up { background-position: -100px -100px; } +.emoji-arrow_down { background-position: -120px 0; } +.emoji-arrow_down_small { background-position: -120px -20px; } +.emoji-arrow_forward { background-position: -120px -40px; } +.emoji-arrow_heading_down { background-position: -120px -60px; } +.emoji-arrow_heading_up { background-position: -120px -80px; } +.emoji-arrow_left { background-position: -120px -100px; } +.emoji-arrow_lower_left { background-position: 0 -120px; } +.emoji-arrow_lower_right { background-position: -20px -120px; } +.emoji-arrow_right { background-position: -40px -120px; } +.emoji-arrow_right_hook { background-position: -60px -120px; } +.emoji-arrow_up { background-position: -80px -120px; } +.emoji-arrow_up_down { background-position: -100px -120px; } +.emoji-arrow_up_small { background-position: -120px -120px; } +.emoji-arrow_upper_left { background-position: -140px 0; } +.emoji-arrow_upper_right { background-position: -140px -20px; } +.emoji-arrows_clockwise { background-position: -140px -40px; } +.emoji-arrows_counterclockwise { background-position: -140px -60px; } +.emoji-art { background-position: -140px -80px; } +.emoji-articulated_lorry { background-position: -140px -100px; } +.emoji-asterisk { background-position: -140px -120px; } +.emoji-astonished { background-position: 0 -140px; } +.emoji-athletic_shoe { background-position: -20px -140px; } +.emoji-atm { background-position: -40px -140px; } +.emoji-atom { background-position: -60px -140px; } +.emoji-avocado { background-position: -80px -140px; } +.emoji-b { background-position: -100px -140px; } +.emoji-baby { background-position: -120px -140px; } +.emoji-baby_bottle { background-position: -140px -140px; } +.emoji-baby_chick { background-position: -160px 0; } +.emoji-baby_symbol { background-position: -160px -20px; } +.emoji-baby_tone1 { background-position: -160px -40px; } +.emoji-baby_tone2 { background-position: -160px -60px; } +.emoji-baby_tone3 { background-position: -160px -80px; } +.emoji-baby_tone4 { background-position: -160px -100px; } +.emoji-baby_tone5 { background-position: -160px -120px; } +.emoji-back { background-position: -160px -140px; } +.emoji-bacon { background-position: 0 -160px; } +.emoji-badminton { background-position: -20px -160px; } +.emoji-baggage_claim { background-position: -40px -160px; } +.emoji-balloon { background-position: -60px -160px; } +.emoji-ballot_box { background-position: -80px -160px; } +.emoji-ballot_box_with_check { background-position: -100px -160px; } +.emoji-bamboo { background-position: -120px -160px; } +.emoji-banana { background-position: -140px -160px; } +.emoji-bangbang { background-position: -160px -160px; } +.emoji-bank { background-position: -180px 0; } +.emoji-bar_chart { background-position: -180px -20px; } +.emoji-barber { background-position: -180px -40px; } +.emoji-baseball { background-position: -180px -60px; } +.emoji-basketball { background-position: -180px -80px; } +.emoji-basketball_player { background-position: -180px -100px; } +.emoji-basketball_player_tone1 { background-position: -180px -120px; } +.emoji-basketball_player_tone2 { background-position: -180px -140px; } +.emoji-basketball_player_tone3 { background-position: -180px -160px; } +.emoji-basketball_player_tone4 { background-position: 0 -180px; } +.emoji-basketball_player_tone5 { background-position: -20px -180px; } +.emoji-bat { background-position: -40px -180px; } +.emoji-bath { background-position: -60px -180px; } +.emoji-bath_tone1 { background-position: -80px -180px; } +.emoji-bath_tone2 { background-position: -100px -180px; } +.emoji-bath_tone3 { background-position: -120px -180px; } +.emoji-bath_tone4 { background-position: -140px -180px; } +.emoji-bath_tone5 { background-position: -160px -180px; } +.emoji-bathtub { background-position: -180px -180px; } +.emoji-battery { background-position: -200px 0; } +.emoji-beach { background-position: -200px -20px; } +.emoji-beach_umbrella { background-position: -200px -40px; } +.emoji-bear { background-position: -200px -60px; } +.emoji-bed { background-position: -200px -80px; } +.emoji-bee { background-position: -200px -100px; } +.emoji-beer { background-position: -200px -120px; } +.emoji-beers { background-position: -200px -140px; } +.emoji-beetle { background-position: -200px -160px; } +.emoji-beginner { background-position: -200px -180px; } +.emoji-bell { background-position: 0 -200px; } +.emoji-bellhop { background-position: -20px -200px; } +.emoji-bento { background-position: -40px -200px; } +.emoji-bicyclist { background-position: -60px -200px; } +.emoji-bicyclist_tone1 { background-position: -80px -200px; } +.emoji-bicyclist_tone2 { background-position: -100px -200px; } +.emoji-bicyclist_tone3 { background-position: -120px -200px; } +.emoji-bicyclist_tone4 { background-position: -140px -200px; } +.emoji-bicyclist_tone5 { background-position: -160px -200px; } +.emoji-bike { background-position: -180px -200px; } +.emoji-bikini { background-position: -200px -200px; } +.emoji-biohazard { background-position: -220px 0; } +.emoji-bird { background-position: -220px -20px; } +.emoji-birthday { background-position: -220px -40px; } +.emoji-black_circle { background-position: -220px -60px; } +.emoji-black_heart { background-position: -220px -80px; } +.emoji-black_joker { background-position: -220px -100px; } +.emoji-black_large_square { background-position: -220px -120px; } +.emoji-black_medium_small_square { background-position: -220px -140px; } +.emoji-black_medium_square { background-position: -220px -160px; } +.emoji-black_nib { background-position: -220px -180px; } +.emoji-black_small_square { background-position: -220px -200px; } +.emoji-black_square_button { background-position: 0 -220px; } +.emoji-blossom { background-position: -20px -220px; } +.emoji-blowfish { background-position: -40px -220px; } +.emoji-blue_book { background-position: -60px -220px; } +.emoji-blue_car { background-position: -80px -220px; } +.emoji-blue_heart { background-position: -100px -220px; } +.emoji-blush { background-position: -120px -220px; } +.emoji-boar { background-position: -140px -220px; } +.emoji-bomb { background-position: -160px -220px; } +.emoji-book { background-position: -180px -220px; } +.emoji-bookmark { background-position: -200px -220px; } +.emoji-bookmark_tabs { background-position: -220px -220px; } +.emoji-books { background-position: -240px 0; } +.emoji-boom { background-position: -240px -20px; } +.emoji-boot { background-position: -240px -40px; } +.emoji-bouquet { background-position: -240px -60px; } +.emoji-bow { background-position: -240px -80px; } +.emoji-bow_and_arrow { background-position: -240px -100px; } +.emoji-bow_tone1 { background-position: -240px -120px; } +.emoji-bow_tone2 { background-position: -240px -140px; } +.emoji-bow_tone3 { background-position: -240px -160px; } +.emoji-bow_tone4 { background-position: -240px -180px; } +.emoji-bow_tone5 { background-position: -240px -200px; } +.emoji-bowling { background-position: -240px -220px; } +.emoji-boxing_glove { background-position: 0 -240px; } +.emoji-boy { background-position: -20px -240px; } +.emoji-boy_tone1 { background-position: -40px -240px; } +.emoji-boy_tone2 { background-position: -60px -240px; } +.emoji-boy_tone3 { background-position: -80px -240px; } +.emoji-boy_tone4 { background-position: -100px -240px; } +.emoji-boy_tone5 { background-position: -120px -240px; } +.emoji-bread { background-position: -140px -240px; } +.emoji-bride_with_veil { background-position: -160px -240px; } +.emoji-bride_with_veil_tone1 { background-position: -180px -240px; } +.emoji-bride_with_veil_tone2 { background-position: -200px -240px; } +.emoji-bride_with_veil_tone3 { background-position: -220px -240px; } +.emoji-bride_with_veil_tone4 { background-position: -240px -240px; } +.emoji-bride_with_veil_tone5 { background-position: -260px 0; } +.emoji-bridge_at_night { background-position: -260px -20px; } +.emoji-briefcase { background-position: -260px -40px; } +.emoji-broken_heart { background-position: -260px -60px; } +.emoji-bug { background-position: -260px -80px; } +.emoji-bulb { background-position: -260px -100px; } +.emoji-bullettrain_front { background-position: -260px -120px; } +.emoji-bullettrain_side { background-position: -260px -140px; } +.emoji-burrito { background-position: -260px -160px; } +.emoji-bus { background-position: -260px -180px; } +.emoji-busstop { background-position: -260px -200px; } +.emoji-bust_in_silhouette { background-position: -260px -220px; } +.emoji-busts_in_silhouette { background-position: -260px -240px; } +.emoji-butterfly { background-position: 0 -260px; } +.emoji-cactus { background-position: -20px -260px; } +.emoji-cake { background-position: -40px -260px; } +.emoji-calendar { background-position: -60px -260px; } +.emoji-calendar_spiral { background-position: -80px -260px; } +.emoji-call_me { background-position: -100px -260px; } +.emoji-call_me_tone1 { background-position: -120px -260px; } +.emoji-call_me_tone2 { background-position: -140px -260px; } +.emoji-call_me_tone3 { background-position: -160px -260px; } +.emoji-call_me_tone4 { background-position: -180px -260px; } +.emoji-call_me_tone5 { background-position: -200px -260px; } +.emoji-calling { background-position: -220px -260px; } +.emoji-camel { background-position: -240px -260px; } +.emoji-camera { background-position: -260px -260px; } +.emoji-camera_with_flash { background-position: -280px 0; } +.emoji-camping { background-position: -280px -20px; } +.emoji-cancer { background-position: -280px -40px; } +.emoji-candle { background-position: -280px -60px; } +.emoji-candy { background-position: -280px -80px; } +.emoji-canoe { background-position: -280px -100px; } +.emoji-capital_abcd { background-position: -280px -120px; } +.emoji-capricorn { background-position: -280px -140px; } +.emoji-card_box { background-position: -280px -160px; } +.emoji-card_index { background-position: -280px -180px; } +.emoji-carousel_horse { background-position: -280px -200px; } +.emoji-carrot { background-position: -280px -220px; } +.emoji-cartwheel { background-position: -280px -240px; } +.emoji-cartwheel_tone1 { background-position: -280px -260px; } +.emoji-cartwheel_tone2 { background-position: 0 -280px; } +.emoji-cartwheel_tone3 { background-position: -20px -280px; } +.emoji-cartwheel_tone4 { background-position: -40px -280px; } +.emoji-cartwheel_tone5 { background-position: -60px -280px; } +.emoji-cat { background-position: -80px -280px; } +.emoji-cat2 { background-position: -100px -280px; } +.emoji-cd { background-position: -120px -280px; } +.emoji-chains { background-position: -140px -280px; } +.emoji-champagne { background-position: -160px -280px; } +.emoji-champagne_glass { background-position: -180px -280px; } +.emoji-chart { background-position: -200px -280px; } +.emoji-chart_with_downwards_trend { background-position: -220px -280px; } +.emoji-chart_with_upwards_trend { background-position: -240px -280px; } +.emoji-checkered_flag { background-position: -260px -280px; } +.emoji-cheese { background-position: -280px -280px; } +.emoji-cherries { background-position: -300px 0; } +.emoji-cherry_blossom { background-position: -300px -20px; } +.emoji-chestnut { background-position: -300px -40px; } +.emoji-chicken { background-position: -300px -60px; } +.emoji-children_crossing { background-position: -300px -80px; } +.emoji-chipmunk { background-position: -300px -100px; } +.emoji-chocolate_bar { background-position: -300px -120px; } +.emoji-christmas_tree { background-position: -300px -140px; } +.emoji-church { background-position: -300px -160px; } +.emoji-cinema { background-position: -300px -180px; } +.emoji-circus_tent { background-position: -300px -200px; } +.emoji-city_dusk { background-position: -300px -220px; } +.emoji-city_sunset { background-position: -300px -240px; } +.emoji-cityscape { background-position: -300px -260px; } +.emoji-cl { background-position: -300px -280px; } +.emoji-clap { background-position: 0 -300px; } +.emoji-clap_tone1 { background-position: -20px -300px; } +.emoji-clap_tone2 { background-position: -40px -300px; } +.emoji-clap_tone3 { background-position: -60px -300px; } +.emoji-clap_tone4 { background-position: -80px -300px; } +.emoji-clap_tone5 { background-position: -100px -300px; } +.emoji-clapper { background-position: -120px -300px; } +.emoji-classical_building { background-position: -140px -300px; } +.emoji-clipboard { background-position: -160px -300px; } +.emoji-clock { background-position: -180px -300px; } +.emoji-clock1 { background-position: -200px -300px; } +.emoji-clock10 { background-position: -220px -300px; } +.emoji-clock1030 { background-position: -240px -300px; } +.emoji-clock11 { background-position: -260px -300px; } +.emoji-clock1130 { background-position: -280px -300px; } +.emoji-clock12 { background-position: -300px -300px; } +.emoji-clock1230 { background-position: -320px 0; } +.emoji-clock130 { background-position: -320px -20px; } +.emoji-clock2 { background-position: -320px -40px; } +.emoji-clock230 { background-position: -320px -60px; } +.emoji-clock3 { background-position: -320px -80px; } +.emoji-clock330 { background-position: -320px -100px; } +.emoji-clock4 { background-position: -320px -120px; } +.emoji-clock430 { background-position: -320px -140px; } +.emoji-clock5 { background-position: -320px -160px; } +.emoji-clock530 { background-position: -320px -180px; } +.emoji-clock6 { background-position: -320px -200px; } +.emoji-clock630 { background-position: -320px -220px; } +.emoji-clock7 { background-position: -320px -240px; } +.emoji-clock730 { background-position: -320px -260px; } +.emoji-clock8 { background-position: -320px -280px; } +.emoji-clock830 { background-position: -320px -300px; } +.emoji-clock9 { background-position: 0 -320px; } +.emoji-clock930 { background-position: -20px -320px; } +.emoji-closed_book { background-position: -40px -320px; } +.emoji-closed_lock_with_key { background-position: -60px -320px; } +.emoji-closed_umbrella { background-position: -80px -320px; } +.emoji-cloud { background-position: -100px -320px; } +.emoji-cloud_lightning { background-position: -120px -320px; } +.emoji-cloud_rain { background-position: -140px -320px; } +.emoji-cloud_snow { background-position: -160px -320px; } +.emoji-cloud_tornado { background-position: -180px -320px; } +.emoji-clown { background-position: -200px -320px; } +.emoji-clubs { background-position: -220px -320px; } +.emoji-cocktail { background-position: -240px -320px; } +.emoji-coffee { background-position: -260px -320px; } +.emoji-coffin { background-position: -280px -320px; } +.emoji-cold_sweat { background-position: -300px -320px; } +.emoji-comet { background-position: -320px -320px; } +.emoji-compression { background-position: -340px 0; } +.emoji-computer { background-position: -340px -20px; } +.emoji-confetti_ball { background-position: -340px -40px; } +.emoji-confounded { background-position: -340px -60px; } +.emoji-confused { background-position: -340px -80px; } +.emoji-congratulations { background-position: -340px -100px; } +.emoji-construction { background-position: -340px -120px; } +.emoji-construction_site { background-position: -340px -140px; } +.emoji-construction_worker { background-position: -340px -160px; } +.emoji-construction_worker_tone1 { background-position: -340px -180px; } +.emoji-construction_worker_tone2 { background-position: -340px -200px; } +.emoji-construction_worker_tone3 { background-position: -340px -220px; } +.emoji-construction_worker_tone4 { background-position: -340px -240px; } +.emoji-construction_worker_tone5 { background-position: -340px -260px; } +.emoji-control_knobs { background-position: -340px -280px; } +.emoji-convenience_store { background-position: -340px -300px; } +.emoji-cookie { background-position: -340px -320px; } +.emoji-cooking { background-position: 0 -340px; } +.emoji-cool { background-position: -20px -340px; } +.emoji-cop { background-position: -40px -340px; } +.emoji-cop_tone1 { background-position: -60px -340px; } +.emoji-cop_tone2 { background-position: -80px -340px; } +.emoji-cop_tone3 { background-position: -100px -340px; } +.emoji-cop_tone4 { background-position: -120px -340px; } +.emoji-cop_tone5 { background-position: -140px -340px; } +.emoji-copyright { background-position: -160px -340px; } +.emoji-corn { background-position: -180px -340px; } +.emoji-couch { background-position: -200px -340px; } +.emoji-couple { background-position: -220px -340px; } +.emoji-couple_mm { background-position: -240px -340px; } +.emoji-couple_with_heart { background-position: -260px -340px; } +.emoji-couple_ww { background-position: -280px -340px; } +.emoji-couplekiss { background-position: -300px -340px; } +.emoji-cow { background-position: -320px -340px; } +.emoji-cow2 { background-position: -340px -340px; } +.emoji-cowboy { background-position: -360px 0; } +.emoji-crab { background-position: -360px -20px; } +.emoji-crayon { background-position: -360px -40px; } +.emoji-credit_card { background-position: -360px -60px; } +.emoji-crescent_moon { background-position: -360px -80px; } +.emoji-cricket { background-position: -360px -100px; } +.emoji-crocodile { background-position: -360px -120px; } +.emoji-croissant { background-position: -360px -140px; } +.emoji-cross { background-position: -360px -160px; } +.emoji-crossed_flags { background-position: -360px -180px; } +.emoji-crossed_swords { background-position: -360px -200px; } +.emoji-crown { background-position: -360px -220px; } +.emoji-cruise_ship { background-position: -360px -240px; } +.emoji-cry { background-position: -360px -260px; } +.emoji-crying_cat_face { background-position: -360px -280px; } +.emoji-crystal_ball { background-position: -360px -300px; } +.emoji-cucumber { background-position: -360px -320px; } +.emoji-cupid { background-position: -360px -340px; } +.emoji-curly_loop { background-position: 0 -360px; } +.emoji-currency_exchange { background-position: -20px -360px; } +.emoji-curry { background-position: -40px -360px; } +.emoji-custard { background-position: -60px -360px; } +.emoji-customs { background-position: -80px -360px; } +.emoji-cyclone { background-position: -100px -360px; } +.emoji-dagger { background-position: -120px -360px; } +.emoji-dancer { background-position: -140px -360px; } +.emoji-dancer_tone1 { background-position: -160px -360px; } +.emoji-dancer_tone2 { background-position: -180px -360px; } +.emoji-dancer_tone3 { background-position: -200px -360px; } +.emoji-dancer_tone4 { background-position: -220px -360px; } +.emoji-dancer_tone5 { background-position: -240px -360px; } +.emoji-dancers { background-position: -260px -360px; } +.emoji-dango { background-position: -280px -360px; } +.emoji-dark_sunglasses { background-position: -300px -360px; } +.emoji-dart { background-position: -320px -360px; } +.emoji-dash { background-position: -340px -360px; } +.emoji-date { background-position: -360px -360px; } +.emoji-deciduous_tree { background-position: -380px 0; } +.emoji-deer { background-position: -380px -20px; } +.emoji-department_store { background-position: -380px -40px; } +.emoji-desert { background-position: -380px -60px; } +.emoji-desktop { background-position: -380px -80px; } +.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; } +.emoji-diamonds { background-position: -380px -120px; } +.emoji-disappointed { background-position: -380px -140px; } +.emoji-disappointed_relieved { background-position: -380px -160px; } +.emoji-dividers { background-position: -380px -180px; } +.emoji-dizzy { background-position: -380px -200px; } +.emoji-dizzy_face { background-position: -380px -220px; } +.emoji-do_not_litter { background-position: -380px -240px; } +.emoji-dog { background-position: -380px -260px; } +.emoji-dog2 { background-position: -380px -280px; } +.emoji-dollar { background-position: -380px -300px; } +.emoji-dolls { background-position: -380px -320px; } +.emoji-dolphin { background-position: -380px -340px; } +.emoji-door { background-position: -380px -360px; } +.emoji-doughnut { background-position: 0 -380px; } +.emoji-dove { background-position: -20px -380px; } +.emoji-dragon { background-position: -40px -380px; } +.emoji-dragon_face { background-position: -60px -380px; } +.emoji-dress { background-position: -80px -380px; } +.emoji-dromedary_camel { background-position: -100px -380px; } +.emoji-drooling_face { background-position: -120px -380px; } +.emoji-droplet { background-position: -140px -380px; } +.emoji-drum { background-position: -160px -380px; } +.emoji-duck { background-position: -180px -380px; } +.emoji-dvd { background-position: -200px -380px; } +.emoji-e-mail { background-position: -220px -380px; } +.emoji-eagle { background-position: -240px -380px; } +.emoji-ear { background-position: -260px -380px; } +.emoji-ear_of_rice { background-position: -280px -380px; } +.emoji-ear_tone1 { background-position: -300px -380px; } +.emoji-ear_tone2 { background-position: -320px -380px; } +.emoji-ear_tone3 { background-position: -340px -380px; } +.emoji-ear_tone4 { background-position: -360px -380px; } +.emoji-ear_tone5 { background-position: -380px -380px; } +.emoji-earth_africa { background-position: -400px 0; } +.emoji-earth_americas { background-position: -400px -20px; } +.emoji-earth_asia { background-position: -400px -40px; } +.emoji-egg { background-position: -400px -60px; } +.emoji-eggplant { background-position: -400px -80px; } +.emoji-eight { background-position: -400px -100px; } +.emoji-eight_pointed_black_star { background-position: -400px -120px; } +.emoji-eight_spoked_asterisk { background-position: -400px -140px; } +.emoji-eject { background-position: -400px -160px; } +.emoji-electric_plug { background-position: -400px -180px; } +.emoji-elephant { background-position: -400px -200px; } +.emoji-end { background-position: -400px -220px; } +.emoji-envelope { background-position: -400px -240px; } +.emoji-envelope_with_arrow { background-position: -400px -260px; } +.emoji-euro { background-position: -400px -280px; } +.emoji-european_castle { background-position: -400px -300px; } +.emoji-european_post_office { background-position: -400px -320px; } +.emoji-evergreen_tree { background-position: -400px -340px; } +.emoji-exclamation { background-position: -400px -360px; } +.emoji-expressionless { background-position: -400px -380px; } +.emoji-eye { background-position: 0 -400px; } +.emoji-eye_in_speech_bubble { background-position: -20px -400px; } +.emoji-eyeglasses { background-position: -40px -400px; } +.emoji-eyes { background-position: -60px -400px; } +.emoji-face_palm { background-position: -80px -400px; } +.emoji-face_palm_tone1 { background-position: -100px -400px; } +.emoji-face_palm_tone2 { background-position: -120px -400px; } +.emoji-face_palm_tone3 { background-position: -140px -400px; } +.emoji-face_palm_tone4 { background-position: -160px -400px; } +.emoji-face_palm_tone5 { background-position: -180px -400px; } +.emoji-factory { background-position: -200px -400px; } +.emoji-fallen_leaf { background-position: -220px -400px; } +.emoji-family { background-position: -240px -400px; } +.emoji-family_mmb { background-position: -260px -400px; } +.emoji-family_mmbb { background-position: -280px -400px; } +.emoji-family_mmg { background-position: -300px -400px; } +.emoji-family_mmgb { background-position: -320px -400px; } +.emoji-family_mmgg { background-position: -340px -400px; } +.emoji-family_mwbb { background-position: -360px -400px; } +.emoji-family_mwg { background-position: -380px -400px; } +.emoji-family_mwgb { background-position: -400px -400px; } +.emoji-family_mwgg { background-position: -420px 0; } +.emoji-family_wwb { background-position: -420px -20px; } +.emoji-family_wwbb { background-position: -420px -40px; } +.emoji-family_wwg { background-position: -420px -60px; } +.emoji-family_wwgb { background-position: -420px -80px; } +.emoji-family_wwgg { background-position: -420px -100px; } +.emoji-fast_forward { background-position: -420px -120px; } +.emoji-fax { background-position: -420px -140px; } +.emoji-fearful { background-position: -420px -160px; } +.emoji-feet { background-position: -420px -180px; } +.emoji-fencer { background-position: -420px -200px; } +.emoji-ferris_wheel { background-position: -420px -220px; } +.emoji-ferry { background-position: -420px -240px; } +.emoji-field_hockey { background-position: -420px -260px; } +.emoji-file_cabinet { background-position: -420px -280px; } +.emoji-file_folder { background-position: -420px -300px; } +.emoji-film_frames { background-position: -420px -320px; } +.emoji-fingers_crossed { background-position: -420px -340px; } +.emoji-fingers_crossed_tone1 { background-position: -420px -360px; } +.emoji-fingers_crossed_tone2 { background-position: -420px -380px; } +.emoji-fingers_crossed_tone3 { background-position: -420px -400px; } +.emoji-fingers_crossed_tone4 { background-position: 0 -420px; } +.emoji-fingers_crossed_tone5 { background-position: -20px -420px; } +.emoji-fire { background-position: -40px -420px; } +.emoji-fire_engine { background-position: -60px -420px; } +.emoji-fireworks { background-position: -80px -420px; } +.emoji-first_place { background-position: -100px -420px; } +.emoji-first_quarter_moon { background-position: -120px -420px; } +.emoji-first_quarter_moon_with_face { background-position: -140px -420px; } +.emoji-fish { background-position: -160px -420px; } +.emoji-fish_cake { background-position: -180px -420px; } +.emoji-fishing_pole_and_fish { background-position: -200px -420px; } +.emoji-fist { background-position: -220px -420px; } +.emoji-fist_tone1 { background-position: -240px -420px; } +.emoji-fist_tone2 { background-position: -260px -420px; } +.emoji-fist_tone3 { background-position: -280px -420px; } +.emoji-fist_tone4 { background-position: -300px -420px; } +.emoji-fist_tone5 { background-position: -320px -420px; } +.emoji-five { background-position: -340px -420px; } +.emoji-flag_ac { background-position: -360px -420px; } +.emoji-flag_ad { background-position: -380px -420px; } +.emoji-flag_ae { background-position: -400px -420px; } +.emoji-flag_af { background-position: -420px -420px; } +.emoji-flag_ag { background-position: -440px 0; } +.emoji-flag_ai { background-position: -440px -20px; } +.emoji-flag_al { background-position: -440px -40px; } +.emoji-flag_am { background-position: -440px -60px; } +.emoji-flag_ao { background-position: -440px -80px; } +.emoji-flag_aq { background-position: -440px -100px; } +.emoji-flag_ar { background-position: -440px -120px; } +.emoji-flag_as { background-position: -440px -140px; } +.emoji-flag_at { background-position: -440px -160px; } +.emoji-flag_au { background-position: -440px -180px; } +.emoji-flag_aw { background-position: -440px -200px; } +.emoji-flag_ax { background-position: -440px -220px; } +.emoji-flag_az { background-position: -440px -240px; } +.emoji-flag_ba { background-position: -440px -260px; } +.emoji-flag_bb { background-position: -440px -280px; } +.emoji-flag_bd { background-position: -440px -300px; } +.emoji-flag_be { background-position: -440px -320px; } +.emoji-flag_bf { background-position: -440px -340px; } +.emoji-flag_bg { background-position: -440px -360px; } +.emoji-flag_bh { background-position: -440px -380px; } +.emoji-flag_bi { background-position: -440px -400px; } +.emoji-flag_bj { background-position: -440px -420px; } +.emoji-flag_bl { background-position: 0 -440px; } +.emoji-flag_black { background-position: -20px -440px; } +.emoji-flag_bm { background-position: -40px -440px; } +.emoji-flag_bn { background-position: -60px -440px; } +.emoji-flag_bo { background-position: -80px -440px; } +.emoji-flag_bq { background-position: -100px -440px; } +.emoji-flag_br { background-position: -120px -440px; } +.emoji-flag_bs { background-position: -140px -440px; } +.emoji-flag_bt { background-position: -160px -440px; } +.emoji-flag_bv { background-position: -180px -440px; } +.emoji-flag_bw { background-position: -200px -440px; } +.emoji-flag_by { background-position: -220px -440px; } +.emoji-flag_bz { background-position: -240px -440px; } +.emoji-flag_ca { background-position: -260px -440px; } +.emoji-flag_cc { background-position: -280px -440px; } +.emoji-flag_cd { background-position: -300px -440px; } +.emoji-flag_cf { background-position: -320px -440px; } +.emoji-flag_cg { background-position: -340px -440px; } +.emoji-flag_ch { background-position: -360px -440px; } +.emoji-flag_ci { background-position: -380px -440px; } +.emoji-flag_ck { background-position: -400px -440px; } +.emoji-flag_cl { background-position: -420px -440px; } +.emoji-flag_cm { background-position: -440px -440px; } +.emoji-flag_cn { background-position: -460px 0; } +.emoji-flag_co { background-position: -460px -20px; } +.emoji-flag_cp { background-position: -460px -40px; } +.emoji-flag_cr { background-position: -460px -60px; } +.emoji-flag_cu { background-position: -460px -80px; } +.emoji-flag_cv { background-position: -460px -100px; } +.emoji-flag_cw { background-position: -460px -120px; } +.emoji-flag_cx { background-position: -460px -140px; } +.emoji-flag_cy { background-position: -460px -160px; } +.emoji-flag_cz { background-position: -460px -180px; } +.emoji-flag_de { background-position: -460px -200px; } +.emoji-flag_dg { background-position: -460px -220px; } +.emoji-flag_dj { background-position: -460px -240px; } +.emoji-flag_dk { background-position: -460px -260px; } +.emoji-flag_dm { background-position: -460px -280px; } +.emoji-flag_do { background-position: -460px -300px; } +.emoji-flag_dz { background-position: -460px -320px; } +.emoji-flag_ea { background-position: -460px -340px; } +.emoji-flag_ec { background-position: -460px -360px; } +.emoji-flag_ee { background-position: -460px -380px; } +.emoji-flag_eg { background-position: -460px -400px; } +.emoji-flag_eh { background-position: -460px -420px; } +.emoji-flag_er { background-position: -460px -440px; } +.emoji-flag_es { background-position: 0 -460px; } +.emoji-flag_et { background-position: -20px -460px; } +.emoji-flag_eu { background-position: -40px -460px; } +.emoji-flag_fi { background-position: -60px -460px; } +.emoji-flag_fj { background-position: -80px -460px; } +.emoji-flag_fk { background-position: -100px -460px; } +.emoji-flag_fm { background-position: -120px -460px; } +.emoji-flag_fo { background-position: -140px -460px; } +.emoji-flag_fr { background-position: -160px -460px; } +.emoji-flag_ga { background-position: -180px -460px; } +.emoji-flag_gb { background-position: -200px -460px; } +.emoji-flag_gd { background-position: -220px -460px; } +.emoji-flag_ge { background-position: -240px -460px; } +.emoji-flag_gf { background-position: -260px -460px; } +.emoji-flag_gg { background-position: -280px -460px; } +.emoji-flag_gh { background-position: -300px -460px; } +.emoji-flag_gi { background-position: -320px -460px; } +.emoji-flag_gl { background-position: -340px -460px; } +.emoji-flag_gm { background-position: -360px -460px; } +.emoji-flag_gn { background-position: -380px -460px; } +.emoji-flag_gp { background-position: -400px -460px; } +.emoji-flag_gq { background-position: -420px -460px; } +.emoji-flag_gr { background-position: -440px -460px; } +.emoji-flag_gs { background-position: -460px -460px; } +.emoji-flag_gt { background-position: -480px 0; } +.emoji-flag_gu { background-position: -480px -20px; } +.emoji-flag_gw { background-position: -480px -40px; } +.emoji-flag_gy { background-position: -480px -60px; } +.emoji-flag_hk { background-position: -480px -80px; } +.emoji-flag_hm { background-position: -480px -100px; } +.emoji-flag_hn { background-position: -480px -120px; } +.emoji-flag_hr { background-position: -480px -140px; } +.emoji-flag_ht { background-position: -480px -160px; } +.emoji-flag_hu { background-position: -480px -180px; } +.emoji-flag_ic { background-position: -480px -200px; } +.emoji-flag_id { background-position: -480px -220px; } +.emoji-flag_ie { background-position: -480px -240px; } +.emoji-flag_il { background-position: -480px -260px; } +.emoji-flag_im { background-position: -480px -280px; } +.emoji-flag_in { background-position: -480px -300px; } +.emoji-flag_io { background-position: -480px -320px; } +.emoji-flag_iq { background-position: -480px -340px; } +.emoji-flag_ir { background-position: -480px -360px; } +.emoji-flag_is { background-position: -480px -380px; } +.emoji-flag_it { background-position: -480px -400px; } +.emoji-flag_je { background-position: -480px -420px; } +.emoji-flag_jm { background-position: -480px -440px; } +.emoji-flag_jo { background-position: -480px -460px; } +.emoji-flag_jp { background-position: 0 -480px; } +.emoji-flag_ke { background-position: -20px -480px; } +.emoji-flag_kg { background-position: -40px -480px; } +.emoji-flag_kh { background-position: -60px -480px; } +.emoji-flag_ki { background-position: -80px -480px; } +.emoji-flag_km { background-position: -100px -480px; } +.emoji-flag_kn { background-position: -120px -480px; } +.emoji-flag_kp { background-position: -140px -480px; } +.emoji-flag_kr { background-position: -160px -480px; } +.emoji-flag_kw { background-position: -180px -480px; } +.emoji-flag_ky { background-position: -200px -480px; } +.emoji-flag_kz { background-position: -220px -480px; } +.emoji-flag_la { background-position: -240px -480px; } +.emoji-flag_lb { background-position: -260px -480px; } +.emoji-flag_lc { background-position: -280px -480px; } +.emoji-flag_li { background-position: -300px -480px; } +.emoji-flag_lk { background-position: -320px -480px; } +.emoji-flag_lr { background-position: -340px -480px; } +.emoji-flag_ls { background-position: -360px -480px; } +.emoji-flag_lt { background-position: -380px -480px; } +.emoji-flag_lu { background-position: -400px -480px; } +.emoji-flag_lv { background-position: -420px -480px; } +.emoji-flag_ly { background-position: -440px -480px; } +.emoji-flag_ma { background-position: -460px -480px; } +.emoji-flag_mc { background-position: -480px -480px; } +.emoji-flag_md { background-position: -500px 0; } +.emoji-flag_me { background-position: -500px -20px; } +.emoji-flag_mf { background-position: -500px -40px; } +.emoji-flag_mg { background-position: -500px -60px; } +.emoji-flag_mh { background-position: -500px -80px; } +.emoji-flag_mk { background-position: -500px -100px; } +.emoji-flag_ml { background-position: -500px -120px; } +.emoji-flag_mm { background-position: -500px -140px; } +.emoji-flag_mn { background-position: -500px -160px; } +.emoji-flag_mo { background-position: -500px -180px; } +.emoji-flag_mp { background-position: -500px -200px; } +.emoji-flag_mq { background-position: -500px -220px; } +.emoji-flag_mr { background-position: -500px -240px; } +.emoji-flag_ms { background-position: -500px -260px; } +.emoji-flag_mt { background-position: -500px -280px; } +.emoji-flag_mu { background-position: -500px -300px; } +.emoji-flag_mv { background-position: -500px -320px; } +.emoji-flag_mw { background-position: -500px -340px; } +.emoji-flag_mx { background-position: -500px -360px; } +.emoji-flag_my { background-position: -500px -380px; } +.emoji-flag_mz { background-position: -500px -400px; } +.emoji-flag_na { background-position: -500px -420px; } +.emoji-flag_nc { background-position: -500px -440px; } +.emoji-flag_ne { background-position: -500px -460px; } +.emoji-flag_nf { background-position: -500px -480px; } +.emoji-flag_ng { background-position: 0 -500px; } +.emoji-flag_ni { background-position: -20px -500px; } +.emoji-flag_nl { background-position: -40px -500px; } +.emoji-flag_no { background-position: -60px -500px; } +.emoji-flag_np { background-position: -80px -500px; } +.emoji-flag_nr { background-position: -100px -500px; } +.emoji-flag_nu { background-position: -120px -500px; } +.emoji-flag_nz { background-position: -140px -500px; } +.emoji-flag_om { background-position: -160px -500px; } +.emoji-flag_pa { background-position: -180px -500px; } +.emoji-flag_pe { background-position: -200px -500px; } +.emoji-flag_pf { background-position: -220px -500px; } +.emoji-flag_pg { background-position: -240px -500px; } +.emoji-flag_ph { background-position: -260px -500px; } +.emoji-flag_pk { background-position: -280px -500px; } +.emoji-flag_pl { background-position: -300px -500px; } +.emoji-flag_pm { background-position: -320px -500px; } +.emoji-flag_pn { background-position: -340px -500px; } +.emoji-flag_pr { background-position: -360px -500px; } +.emoji-flag_ps { background-position: -380px -500px; } +.emoji-flag_pt { background-position: -400px -500px; } +.emoji-flag_pw { background-position: -420px -500px; } +.emoji-flag_py { background-position: -440px -500px; } +.emoji-flag_qa { background-position: -460px -500px; } +.emoji-flag_re { background-position: -480px -500px; } +.emoji-flag_ro { background-position: -500px -500px; } +.emoji-flag_rs { background-position: -520px 0; } +.emoji-flag_ru { background-position: -520px -20px; } +.emoji-flag_rw { background-position: -520px -40px; } +.emoji-flag_sa { background-position: -520px -60px; } +.emoji-flag_sb { background-position: -520px -80px; } +.emoji-flag_sc { background-position: -520px -100px; } +.emoji-flag_sd { background-position: -520px -120px; } +.emoji-flag_se { background-position: -520px -140px; } +.emoji-flag_sg { background-position: -520px -160px; } +.emoji-flag_sh { background-position: -520px -180px; } +.emoji-flag_si { background-position: -520px -200px; } +.emoji-flag_sj { background-position: -520px -220px; } +.emoji-flag_sk { background-position: -520px -240px; } +.emoji-flag_sl { background-position: -520px -260px; } +.emoji-flag_sm { background-position: -520px -280px; } +.emoji-flag_sn { background-position: -520px -300px; } +.emoji-flag_so { background-position: -520px -320px; } +.emoji-flag_sr { background-position: -520px -340px; } +.emoji-flag_ss { background-position: -520px -360px; } +.emoji-flag_st { background-position: -520px -380px; } +.emoji-flag_sv { background-position: -520px -400px; } +.emoji-flag_sx { background-position: -520px -420px; } +.emoji-flag_sy { background-position: -520px -440px; } +.emoji-flag_sz { background-position: -520px -460px; } +.emoji-flag_ta { background-position: -520px -480px; } +.emoji-flag_tc { background-position: -520px -500px; } +.emoji-flag_td { background-position: 0 -520px; } +.emoji-flag_tf { background-position: -20px -520px; } +.emoji-flag_tg { background-position: -40px -520px; } +.emoji-flag_th { background-position: -60px -520px; } +.emoji-flag_tj { background-position: -80px -520px; } +.emoji-flag_tk { background-position: -100px -520px; } +.emoji-flag_tl { background-position: -120px -520px; } +.emoji-flag_tm { background-position: -140px -520px; } +.emoji-flag_tn { background-position: -160px -520px; } +.emoji-flag_to { background-position: -180px -520px; } +.emoji-flag_tr { background-position: -200px -520px; } +.emoji-flag_tt { background-position: -220px -520px; } +.emoji-flag_tv { background-position: -240px -520px; } +.emoji-flag_tw { background-position: -260px -520px; } +.emoji-flag_tz { background-position: -280px -520px; } +.emoji-flag_ua { background-position: -300px -520px; } +.emoji-flag_ug { background-position: -320px -520px; } +.emoji-flag_um { background-position: -340px -520px; } +.emoji-flag_us { background-position: -360px -520px; } +.emoji-flag_uy { background-position: -380px -520px; } +.emoji-flag_uz { background-position: -400px -520px; } +.emoji-flag_va { background-position: -420px -520px; } +.emoji-flag_vc { background-position: -440px -520px; } +.emoji-flag_ve { background-position: -460px -520px; } +.emoji-flag_vg { background-position: -480px -520px; } +.emoji-flag_vi { background-position: -500px -520px; } +.emoji-flag_vn { background-position: -520px -520px; } +.emoji-flag_vu { background-position: -540px 0; } +.emoji-flag_wf { background-position: -540px -20px; } +.emoji-flag_white { background-position: -540px -40px; } +.emoji-flag_ws { background-position: -540px -60px; } +.emoji-flag_xk { background-position: -540px -80px; } +.emoji-flag_ye { background-position: -540px -100px; } +.emoji-flag_yt { background-position: -540px -120px; } +.emoji-flag_za { background-position: -540px -140px; } +.emoji-flag_zm { background-position: -540px -160px; } +.emoji-flag_zw { background-position: -540px -180px; } +.emoji-flags { background-position: -540px -200px; } +.emoji-flashlight { background-position: -540px -220px; } +.emoji-fleur-de-lis { background-position: -540px -240px; } +.emoji-floppy_disk { background-position: -540px -260px; } +.emoji-flower_playing_cards { background-position: -540px -280px; } +.emoji-flushed { background-position: -540px -300px; } +.emoji-fog { background-position: -540px -320px; } +.emoji-foggy { background-position: -540px -340px; } +.emoji-football { background-position: -540px -360px; } +.emoji-footprints { background-position: -540px -380px; } +.emoji-fork_and_knife { background-position: -540px -400px; } +.emoji-fork_knife_plate { background-position: -540px -420px; } +.emoji-fountain { background-position: -540px -440px; } +.emoji-four { background-position: -540px -460px; } +.emoji-four_leaf_clover { background-position: -540px -480px; } +.emoji-fox { background-position: -540px -500px; } +.emoji-frame_photo { background-position: -540px -520px; } +.emoji-free { background-position: 0 -540px; } +.emoji-french_bread { background-position: -20px -540px; } +.emoji-fried_shrimp { background-position: -40px -540px; } +.emoji-fries { background-position: -60px -540px; } +.emoji-frog { background-position: -80px -540px; } +.emoji-frowning { background-position: -100px -540px; } +.emoji-frowning2 { background-position: -120px -540px; } +.emoji-fuelpump { background-position: -140px -540px; } +.emoji-full_moon { background-position: -160px -540px; } +.emoji-full_moon_with_face { background-position: -180px -540px; } +.emoji-game_die { background-position: -200px -540px; } +.emoji-gear { background-position: -220px -540px; } +.emoji-gem { background-position: -240px -540px; } +.emoji-gemini { background-position: -260px -540px; } +.emoji-ghost { background-position: -280px -540px; } +.emoji-gift { background-position: -300px -540px; } +.emoji-gift_heart { background-position: -320px -540px; } +.emoji-girl { background-position: -340px -540px; } +.emoji-girl_tone1 { background-position: -360px -540px; } +.emoji-girl_tone2 { background-position: -380px -540px; } +.emoji-girl_tone3 { background-position: -400px -540px; } +.emoji-girl_tone4 { background-position: -420px -540px; } +.emoji-girl_tone5 { background-position: -440px -540px; } +.emoji-globe_with_meridians { background-position: -460px -540px; } +.emoji-goal { background-position: -480px -540px; } +.emoji-goat { background-position: -500px -540px; } +.emoji-golf { background-position: -520px -540px; } +.emoji-golfer { background-position: -540px -540px; } +.emoji-gorilla { background-position: -560px 0; } +.emoji-grapes { background-position: -560px -20px; } +.emoji-green_apple { background-position: -560px -40px; } +.emoji-green_book { background-position: -560px -60px; } +.emoji-green_heart { background-position: -560px -80px; } +.emoji-grey_exclamation { background-position: -560px -100px; } +.emoji-grey_question { background-position: -560px -120px; } +.emoji-grimacing { background-position: -560px -140px; } +.emoji-grin { background-position: -560px -160px; } +.emoji-grinning { background-position: -560px -180px; } +.emoji-guardsman { background-position: -560px -200px; } +.emoji-guardsman_tone1 { background-position: -560px -220px; } +.emoji-guardsman_tone2 { background-position: -560px -240px; } +.emoji-guardsman_tone3 { background-position: -560px -260px; } +.emoji-guardsman_tone4 { background-position: -560px -280px; } +.emoji-guardsman_tone5 { background-position: -560px -300px; } +.emoji-guitar { background-position: -560px -320px; } +.emoji-gun { background-position: -560px -340px; } +.emoji-haircut { background-position: -560px -360px; } +.emoji-haircut_tone1 { background-position: -560px -380px; } +.emoji-haircut_tone2 { background-position: -560px -400px; } +.emoji-haircut_tone3 { background-position: -560px -420px; } +.emoji-haircut_tone4 { background-position: -560px -440px; } +.emoji-haircut_tone5 { background-position: -560px -460px; } +.emoji-hamburger { background-position: -560px -480px; } +.emoji-hammer { background-position: -560px -500px; } +.emoji-hammer_pick { background-position: -560px -520px; } +.emoji-hamster { background-position: -560px -540px; } +.emoji-hand_splayed { background-position: 0 -560px; } +.emoji-hand_splayed_tone1 { background-position: -20px -560px; } +.emoji-hand_splayed_tone2 { background-position: -40px -560px; } +.emoji-hand_splayed_tone3 { background-position: -60px -560px; } +.emoji-hand_splayed_tone4 { background-position: -80px -560px; } +.emoji-hand_splayed_tone5 { background-position: -100px -560px; } +.emoji-handbag { background-position: -120px -560px; } +.emoji-handball { background-position: -140px -560px; } +.emoji-handball_tone1 { background-position: -160px -560px; } +.emoji-handball_tone2 { background-position: -180px -560px; } +.emoji-handball_tone3 { background-position: -200px -560px; } +.emoji-handball_tone4 { background-position: -220px -560px; } +.emoji-handball_tone5 { background-position: -240px -560px; } +.emoji-handshake { background-position: -260px -560px; } +.emoji-handshake_tone1 { background-position: -280px -560px; } +.emoji-handshake_tone2 { background-position: -300px -560px; } +.emoji-handshake_tone3 { background-position: -320px -560px; } +.emoji-handshake_tone4 { background-position: -340px -560px; } +.emoji-handshake_tone5 { background-position: -360px -560px; } +.emoji-hash { background-position: -380px -560px; } +.emoji-hatched_chick { background-position: -400px -560px; } +.emoji-hatching_chick { background-position: -420px -560px; } +.emoji-head_bandage { background-position: -440px -560px; } +.emoji-headphones { background-position: -460px -560px; } +.emoji-hear_no_evil { background-position: -480px -560px; } +.emoji-heart { background-position: -500px -560px; } +.emoji-heart_decoration { background-position: -520px -560px; } +.emoji-heart_exclamation { background-position: -540px -560px; } +.emoji-heart_eyes { background-position: -560px -560px; } +.emoji-heart_eyes_cat { background-position: -580px 0; } +.emoji-heartbeat { background-position: -580px -20px; } +.emoji-heartpulse { background-position: -580px -40px; } +.emoji-hearts { background-position: -580px -60px; } +.emoji-heavy_check_mark { background-position: -580px -80px; } +.emoji-heavy_division_sign { background-position: -580px -100px; } +.emoji-heavy_dollar_sign { background-position: -580px -120px; } +.emoji-heavy_minus_sign { background-position: -580px -140px; } +.emoji-heavy_multiplication_x { background-position: -580px -160px; } +.emoji-heavy_plus_sign { background-position: -580px -180px; } +.emoji-helicopter { background-position: -580px -200px; } +.emoji-helmet_with_cross { background-position: -580px -220px; } +.emoji-herb { background-position: -580px -240px; } +.emoji-hibiscus { background-position: -580px -260px; } +.emoji-high_brightness { background-position: -580px -280px; } +.emoji-high_heel { background-position: -580px -300px; } +.emoji-hockey { background-position: -580px -320px; } +.emoji-hole { background-position: -580px -340px; } +.emoji-homes { background-position: -580px -360px; } +.emoji-honey_pot { background-position: -580px -380px; } +.emoji-horse { background-position: -580px -400px; } +.emoji-horse_racing { background-position: -580px -420px; } +.emoji-horse_racing_tone1 { background-position: -580px -440px; } +.emoji-horse_racing_tone2 { background-position: -580px -460px; } +.emoji-horse_racing_tone3 { background-position: -580px -480px; } +.emoji-horse_racing_tone4 { background-position: -580px -500px; } +.emoji-horse_racing_tone5 { background-position: -580px -520px; } +.emoji-hospital { background-position: -580px -540px; } +.emoji-hot_pepper { background-position: -580px -560px; } +.emoji-hotdog { background-position: 0 -580px; } +.emoji-hotel { background-position: -20px -580px; } +.emoji-hotsprings { background-position: -40px -580px; } +.emoji-hourglass { background-position: -60px -580px; } +.emoji-hourglass_flowing_sand { background-position: -80px -580px; } +.emoji-house { background-position: -100px -580px; } +.emoji-house_abandoned { background-position: -120px -580px; } +.emoji-house_with_garden { background-position: -140px -580px; } +.emoji-hugging { background-position: -160px -580px; } +.emoji-hushed { background-position: -180px -580px; } +.emoji-ice_cream { background-position: -200px -580px; } +.emoji-ice_skate { background-position: -220px -580px; } +.emoji-icecream { background-position: -240px -580px; } +.emoji-id { background-position: -260px -580px; } +.emoji-ideograph_advantage { background-position: -280px -580px; } +.emoji-imp { background-position: -300px -580px; } +.emoji-inbox_tray { background-position: -320px -580px; } +.emoji-incoming_envelope { background-position: -340px -580px; } +.emoji-information_desk_person { background-position: -360px -580px; } +.emoji-information_desk_person_tone1 { background-position: -380px -580px; } +.emoji-information_desk_person_tone2 { background-position: -400px -580px; } +.emoji-information_desk_person_tone3 { background-position: -420px -580px; } +.emoji-information_desk_person_tone4 { background-position: -440px -580px; } +.emoji-information_desk_person_tone5 { background-position: -460px -580px; } +.emoji-information_source { background-position: -480px -580px; } +.emoji-innocent { background-position: -500px -580px; } +.emoji-interrobang { background-position: -520px -580px; } +.emoji-iphone { background-position: -540px -580px; } +.emoji-island { background-position: -560px -580px; } +.emoji-izakaya_lantern { background-position: -580px -580px; } +.emoji-jack_o_lantern { background-position: -600px 0; } +.emoji-japan { background-position: -600px -20px; } +.emoji-japanese_castle { background-position: -600px -40px; } +.emoji-japanese_goblin { background-position: -600px -60px; } +.emoji-japanese_ogre { background-position: -600px -80px; } +.emoji-jeans { background-position: -600px -100px; } +.emoji-joy { background-position: -600px -120px; } +.emoji-joy_cat { background-position: -600px -140px; } +.emoji-joystick { background-position: -600px -160px; } +.emoji-juggling { background-position: -600px -180px; } +.emoji-juggling_tone1 { background-position: -600px -200px; } +.emoji-juggling_tone2 { background-position: -600px -220px; } +.emoji-juggling_tone3 { background-position: -600px -240px; } +.emoji-juggling_tone4 { background-position: -600px -260px; } +.emoji-juggling_tone5 { background-position: -600px -280px; } +.emoji-kaaba { background-position: -600px -300px; } +.emoji-key { background-position: -600px -320px; } +.emoji-key2 { background-position: -600px -340px; } +.emoji-keyboard { background-position: -600px -360px; } +.emoji-kimono { background-position: -600px -380px; } +.emoji-kiss { background-position: -600px -400px; } +.emoji-kiss_mm { background-position: -600px -420px; } +.emoji-kiss_ww { background-position: -600px -440px; } +.emoji-kissing { background-position: -600px -460px; } +.emoji-kissing_cat { background-position: -600px -480px; } +.emoji-kissing_closed_eyes { background-position: -600px -500px; } +.emoji-kissing_heart { background-position: -600px -520px; } +.emoji-kissing_smiling_eyes { background-position: -600px -540px; } +.emoji-kiwi { background-position: -600px -560px; } +.emoji-knife { background-position: -600px -580px; } +.emoji-koala { background-position: 0 -600px; } +.emoji-koko { background-position: -20px -600px; } +.emoji-label { background-position: -40px -600px; } +.emoji-large_blue_circle { background-position: -60px -600px; } +.emoji-large_blue_diamond { background-position: -80px -600px; } +.emoji-large_orange_diamond { background-position: -100px -600px; } +.emoji-last_quarter_moon { background-position: -120px -600px; } +.emoji-last_quarter_moon_with_face { background-position: -140px -600px; } +.emoji-laughing { background-position: -160px -600px; } +.emoji-leaves { background-position: -180px -600px; } +.emoji-ledger { background-position: -200px -600px; } +.emoji-left_facing_fist { background-position: -220px -600px; } +.emoji-left_facing_fist_tone1 { background-position: -240px -600px; } +.emoji-left_facing_fist_tone2 { background-position: -260px -600px; } +.emoji-left_facing_fist_tone3 { background-position: -280px -600px; } +.emoji-left_facing_fist_tone4 { background-position: -300px -600px; } +.emoji-left_facing_fist_tone5 { background-position: -320px -600px; } +.emoji-left_luggage { background-position: -340px -600px; } +.emoji-left_right_arrow { background-position: -360px -600px; } +.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; } +.emoji-lemon { background-position: -400px -600px; } +.emoji-leo { background-position: -420px -600px; } +.emoji-leopard { background-position: -440px -600px; } +.emoji-level_slider { background-position: -460px -600px; } +.emoji-levitate { background-position: -480px -600px; } +.emoji-libra { background-position: -500px -600px; } +.emoji-lifter { background-position: -520px -600px; } +.emoji-lifter_tone1 { background-position: -540px -600px; } +.emoji-lifter_tone2 { background-position: -560px -600px; } +.emoji-lifter_tone3 { background-position: -580px -600px; } +.emoji-lifter_tone4 { background-position: -600px -600px; } +.emoji-lifter_tone5 { background-position: -620px 0; } +.emoji-light_rail { background-position: -620px -20px; } +.emoji-link { background-position: -620px -40px; } +.emoji-lion_face { background-position: -620px -60px; } +.emoji-lips { background-position: -620px -80px; } +.emoji-lipstick { background-position: -620px -100px; } +.emoji-lizard { background-position: -620px -120px; } +.emoji-lock { background-position: -620px -140px; } +.emoji-lock_with_ink_pen { background-position: -620px -160px; } +.emoji-lollipop { background-position: -620px -180px; } +.emoji-loop { background-position: -620px -200px; } +.emoji-loud_sound { background-position: -620px -220px; } +.emoji-loudspeaker { background-position: -620px -240px; } +.emoji-love_hotel { background-position: -620px -260px; } +.emoji-love_letter { background-position: -620px -280px; } +.emoji-low_brightness { background-position: -620px -300px; } +.emoji-lying_face { background-position: -620px -320px; } +.emoji-m { background-position: -620px -340px; } +.emoji-mag { background-position: -620px -360px; } +.emoji-mag_right { background-position: -620px -380px; } +.emoji-mahjong { background-position: -620px -400px; } +.emoji-mailbox { background-position: -620px -420px; } +.emoji-mailbox_closed { background-position: -620px -440px; } +.emoji-mailbox_with_mail { background-position: -620px -460px; } +.emoji-mailbox_with_no_mail { background-position: -620px -480px; } +.emoji-man { background-position: -620px -500px; } +.emoji-man_dancing { background-position: -620px -520px; } +.emoji-man_dancing_tone1 { background-position: -620px -540px; } +.emoji-man_dancing_tone2 { background-position: -620px -560px; } +.emoji-man_dancing_tone3 { background-position: -620px -580px; } +.emoji-man_dancing_tone4 { background-position: -620px -600px; } +.emoji-man_dancing_tone5 { background-position: 0 -620px; } +.emoji-man_in_tuxedo { background-position: -20px -620px; } +.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; } +.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; } +.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; } +.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; } +.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; } +.emoji-man_tone1 { background-position: -140px -620px; } +.emoji-man_tone2 { background-position: -160px -620px; } +.emoji-man_tone3 { background-position: -180px -620px; } +.emoji-man_tone4 { background-position: -200px -620px; } +.emoji-man_tone5 { background-position: -220px -620px; } +.emoji-man_with_gua_pi_mao { background-position: -240px -620px; } +.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; } +.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; } +.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; } +.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; } +.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; } +.emoji-man_with_turban { background-position: -360px -620px; } +.emoji-man_with_turban_tone1 { background-position: -380px -620px; } +.emoji-man_with_turban_tone2 { background-position: -400px -620px; } +.emoji-man_with_turban_tone3 { background-position: -420px -620px; } +.emoji-man_with_turban_tone4 { background-position: -440px -620px; } +.emoji-man_with_turban_tone5 { background-position: -460px -620px; } +.emoji-mans_shoe { background-position: -480px -620px; } +.emoji-map { background-position: -500px -620px; } +.emoji-maple_leaf { background-position: -520px -620px; } +.emoji-martial_arts_uniform { background-position: -540px -620px; } +.emoji-mask { background-position: -560px -620px; } +.emoji-massage { background-position: -580px -620px; } +.emoji-massage_tone1 { background-position: -600px -620px; } +.emoji-massage_tone2 { background-position: -620px -620px; } +.emoji-massage_tone3 { background-position: -640px 0; } +.emoji-massage_tone4 { background-position: -640px -20px; } +.emoji-massage_tone5 { background-position: -640px -40px; } +.emoji-meat_on_bone { background-position: -640px -60px; } +.emoji-medal { background-position: -640px -80px; } +.emoji-mega { background-position: -640px -100px; } +.emoji-melon { background-position: -640px -120px; } +.emoji-menorah { background-position: -640px -140px; } +.emoji-mens { background-position: -640px -160px; } +.emoji-metal { background-position: -640px -180px; } +.emoji-metal_tone1 { background-position: -640px -200px; } +.emoji-metal_tone2 { background-position: -640px -220px; } +.emoji-metal_tone3 { background-position: -640px -240px; } +.emoji-metal_tone4 { background-position: -640px -260px; } +.emoji-metal_tone5 { background-position: -640px -280px; } +.emoji-metro { background-position: -640px -300px; } +.emoji-microphone { background-position: -640px -320px; } +.emoji-microphone2 { background-position: -640px -340px; } +.emoji-microscope { background-position: -640px -360px; } +.emoji-middle_finger { background-position: -640px -380px; } +.emoji-middle_finger_tone1 { background-position: -640px -400px; } +.emoji-middle_finger_tone2 { background-position: -640px -420px; } +.emoji-middle_finger_tone3 { background-position: -640px -440px; } +.emoji-middle_finger_tone4 { background-position: -640px -460px; } +.emoji-middle_finger_tone5 { background-position: -640px -480px; } +.emoji-military_medal { background-position: -640px -500px; } +.emoji-milk { background-position: -640px -520px; } +.emoji-milky_way { background-position: -640px -540px; } +.emoji-minibus { background-position: -640px -560px; } +.emoji-minidisc { background-position: -640px -580px; } +.emoji-mobile_phone_off { background-position: -640px -600px; } +.emoji-money_mouth { background-position: -640px -620px; } +.emoji-money_with_wings { background-position: 0 -640px; } +.emoji-moneybag { background-position: -20px -640px; } +.emoji-monkey { background-position: -40px -640px; } +.emoji-monkey_face { background-position: -60px -640px; } +.emoji-monorail { background-position: -80px -640px; } +.emoji-mortar_board { background-position: -100px -640px; } +.emoji-mosque { background-position: -120px -640px; } +.emoji-motor_scooter { background-position: -140px -640px; } +.emoji-motorboat { background-position: -160px -640px; } +.emoji-motorcycle { background-position: -180px -640px; } +.emoji-motorway { background-position: -200px -640px; } +.emoji-mount_fuji { background-position: -220px -640px; } +.emoji-mountain { background-position: -240px -640px; } +.emoji-mountain_bicyclist { background-position: -260px -640px; } +.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; } +.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; } +.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; } +.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; } +.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; } +.emoji-mountain_cableway { background-position: -380px -640px; } +.emoji-mountain_railway { background-position: -400px -640px; } +.emoji-mountain_snow { background-position: -420px -640px; } +.emoji-mouse { background-position: -440px -640px; } +.emoji-mouse2 { background-position: -460px -640px; } +.emoji-mouse_three_button { background-position: -480px -640px; } +.emoji-movie_camera { background-position: -500px -640px; } +.emoji-moyai { background-position: -520px -640px; } +.emoji-mrs_claus { background-position: -540px -640px; } +.emoji-mrs_claus_tone1 { background-position: -560px -640px; } +.emoji-mrs_claus_tone2 { background-position: -580px -640px; } +.emoji-mrs_claus_tone3 { background-position: -600px -640px; } +.emoji-mrs_claus_tone4 { background-position: -620px -640px; } +.emoji-mrs_claus_tone5 { background-position: -640px -640px; } +.emoji-muscle { background-position: -660px 0; } +.emoji-muscle_tone1 { background-position: -660px -20px; } +.emoji-muscle_tone2 { background-position: -660px -40px; } +.emoji-muscle_tone3 { background-position: -660px -60px; } +.emoji-muscle_tone4 { background-position: -660px -80px; } +.emoji-muscle_tone5 { background-position: -660px -100px; } +.emoji-mushroom { background-position: -660px -120px; } +.emoji-musical_keyboard { background-position: -660px -140px; } +.emoji-musical_note { background-position: -660px -160px; } +.emoji-musical_score { background-position: -660px -180px; } +.emoji-mute { background-position: -660px -200px; } +.emoji-nail_care { background-position: -660px -220px; } +.emoji-nail_care_tone1 { background-position: -660px -240px; } +.emoji-nail_care_tone2 { background-position: -660px -260px; } +.emoji-nail_care_tone3 { background-position: -660px -280px; } +.emoji-nail_care_tone4 { background-position: -660px -300px; } +.emoji-nail_care_tone5 { background-position: -660px -320px; } +.emoji-name_badge { background-position: -660px -340px; } +.emoji-nauseated_face { background-position: -660px -360px; } +.emoji-necktie { background-position: -660px -380px; } +.emoji-negative_squared_cross_mark { background-position: -660px -400px; } +.emoji-nerd { background-position: -660px -420px; } +.emoji-neutral_face { background-position: -660px -440px; } +.emoji-new { background-position: -660px -460px; } +.emoji-new_moon { background-position: -660px -480px; } +.emoji-new_moon_with_face { background-position: -660px -500px; } +.emoji-newspaper { background-position: -660px -520px; } +.emoji-newspaper2 { background-position: -660px -540px; } +.emoji-ng { background-position: -660px -560px; } +.emoji-night_with_stars { background-position: -660px -580px; } +.emoji-nine { background-position: -660px -600px; } +.emoji-no_bell { background-position: -660px -620px; } +.emoji-no_bicycles { background-position: -660px -640px; } +.emoji-no_entry { background-position: 0 -660px; } +.emoji-no_entry_sign { background-position: -20px -660px; } +.emoji-no_good { background-position: -40px -660px; } +.emoji-no_good_tone1 { background-position: -60px -660px; } +.emoji-no_good_tone2 { background-position: -80px -660px; } +.emoji-no_good_tone3 { background-position: -100px -660px; } +.emoji-no_good_tone4 { background-position: -120px -660px; } +.emoji-no_good_tone5 { background-position: -140px -660px; } +.emoji-no_mobile_phones { background-position: -160px -660px; } +.emoji-no_mouth { background-position: -180px -660px; } +.emoji-no_pedestrians { background-position: -200px -660px; } +.emoji-no_smoking { background-position: -220px -660px; } +.emoji-non-potable_water { background-position: -240px -660px; } +.emoji-nose { background-position: -260px -660px; } +.emoji-nose_tone1 { background-position: -280px -660px; } +.emoji-nose_tone2 { background-position: -300px -660px; } +.emoji-nose_tone3 { background-position: -320px -660px; } +.emoji-nose_tone4 { background-position: -340px -660px; } +.emoji-nose_tone5 { background-position: -360px -660px; } +.emoji-notebook { background-position: -380px -660px; } +.emoji-notebook_with_decorative_cover { background-position: -400px -660px; } +.emoji-notepad_spiral { background-position: -420px -660px; } +.emoji-notes { background-position: -440px -660px; } +.emoji-nut_and_bolt { background-position: -460px -660px; } +.emoji-o { background-position: -480px -660px; } +.emoji-o2 { background-position: -500px -660px; } +.emoji-ocean { background-position: -520px -660px; } +.emoji-octagonal_sign { background-position: -540px -660px; } +.emoji-octopus { background-position: -560px -660px; } +.emoji-oden { background-position: -580px -660px; } +.emoji-office { background-position: -600px -660px; } +.emoji-oil { background-position: -620px -660px; } +.emoji-ok { background-position: -640px -660px; } +.emoji-ok_hand { background-position: -660px -660px; } +.emoji-ok_hand_tone1 { background-position: -680px 0; } +.emoji-ok_hand_tone2 { background-position: -680px -20px; } +.emoji-ok_hand_tone3 { background-position: -680px -40px; } +.emoji-ok_hand_tone4 { background-position: -680px -60px; } +.emoji-ok_hand_tone5 { background-position: -680px -80px; } +.emoji-ok_woman { background-position: -680px -100px; } +.emoji-ok_woman_tone1 { background-position: -680px -120px; } +.emoji-ok_woman_tone2 { background-position: -680px -140px; } +.emoji-ok_woman_tone3 { background-position: -680px -160px; } +.emoji-ok_woman_tone4 { background-position: -680px -180px; } +.emoji-ok_woman_tone5 { background-position: -680px -200px; } +.emoji-older_man { background-position: -680px -220px; } +.emoji-older_man_tone1 { background-position: -680px -240px; } +.emoji-older_man_tone2 { background-position: -680px -260px; } +.emoji-older_man_tone3 { background-position: -680px -280px; } +.emoji-older_man_tone4 { background-position: -680px -300px; } +.emoji-older_man_tone5 { background-position: -680px -320px; } +.emoji-older_woman { background-position: -680px -340px; } +.emoji-older_woman_tone1 { background-position: -680px -360px; } +.emoji-older_woman_tone2 { background-position: -680px -380px; } +.emoji-older_woman_tone3 { background-position: -680px -400px; } +.emoji-older_woman_tone4 { background-position: -680px -420px; } +.emoji-older_woman_tone5 { background-position: -680px -440px; } +.emoji-om_symbol { background-position: -680px -460px; } +.emoji-on { background-position: -680px -480px; } +.emoji-oncoming_automobile { background-position: -680px -500px; } +.emoji-oncoming_bus { background-position: -680px -520px; } +.emoji-oncoming_police_car { background-position: -680px -540px; } +.emoji-oncoming_taxi { background-position: -680px -560px; } +.emoji-one { background-position: -680px -580px; } +.emoji-open_file_folder { background-position: -680px -600px; } +.emoji-open_hands { background-position: -680px -620px; } +.emoji-open_hands_tone1 { background-position: -680px -640px; } +.emoji-open_hands_tone2 { background-position: -680px -660px; } +.emoji-open_hands_tone3 { background-position: 0 -680px; } +.emoji-open_hands_tone4 { background-position: -20px -680px; } +.emoji-open_hands_tone5 { background-position: -40px -680px; } +.emoji-open_mouth { background-position: -60px -680px; } +.emoji-ophiuchus { background-position: -80px -680px; } +.emoji-orange_book { background-position: -100px -680px; } +.emoji-orthodox_cross { background-position: -120px -680px; } +.emoji-outbox_tray { background-position: -140px -680px; } +.emoji-owl { background-position: -160px -680px; } +.emoji-ox { background-position: -180px -680px; } +.emoji-package { background-position: -200px -680px; } +.emoji-page_facing_up { background-position: -220px -680px; } +.emoji-page_with_curl { background-position: -240px -680px; } +.emoji-pager { background-position: -260px -680px; } +.emoji-paintbrush { background-position: -280px -680px; } +.emoji-palm_tree { background-position: -300px -680px; } +.emoji-pancakes { background-position: -320px -680px; } +.emoji-panda_face { background-position: -340px -680px; } +.emoji-paperclip { background-position: -360px -680px; } +.emoji-paperclips { background-position: -380px -680px; } +.emoji-park { background-position: -400px -680px; } +.emoji-parking { background-position: -420px -680px; } +.emoji-part_alternation_mark { background-position: -440px -680px; } +.emoji-partly_sunny { background-position: -460px -680px; } +.emoji-passport_control { background-position: -480px -680px; } +.emoji-pause_button { background-position: -500px -680px; } +.emoji-peace { background-position: -520px -680px; } +.emoji-peach { background-position: -540px -680px; } +.emoji-peanuts { background-position: -560px -680px; } +.emoji-pear { background-position: -580px -680px; } +.emoji-pen_ballpoint { background-position: -600px -680px; } +.emoji-pen_fountain { background-position: -620px -680px; } +.emoji-pencil { background-position: -640px -680px; } +.emoji-pencil2 { background-position: -660px -680px; } +.emoji-penguin { background-position: -680px -680px; } +.emoji-pensive { background-position: -700px 0; } +.emoji-performing_arts { background-position: -700px -20px; } +.emoji-persevere { background-position: -700px -40px; } +.emoji-person_frowning { background-position: -700px -60px; } +.emoji-person_frowning_tone1 { background-position: -700px -80px; } +.emoji-person_frowning_tone2 { background-position: -700px -100px; } +.emoji-person_frowning_tone3 { background-position: -700px -120px; } +.emoji-person_frowning_tone4 { background-position: -700px -140px; } +.emoji-person_frowning_tone5 { background-position: -700px -160px; } +.emoji-person_with_blond_hair { background-position: -700px -180px; } +.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; } +.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; } +.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; } +.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; } +.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; } +.emoji-person_with_pouting_face { background-position: -700px -300px; } +.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; } +.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; } +.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; } +.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; } +.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; } +.emoji-pick { background-position: -700px -420px; } +.emoji-pig { background-position: -700px -440px; } +.emoji-pig2 { background-position: -700px -460px; } +.emoji-pig_nose { background-position: -700px -480px; } +.emoji-pill { background-position: -700px -500px; } +.emoji-pineapple { background-position: -700px -520px; } +.emoji-ping_pong { background-position: -700px -540px; } +.emoji-pisces { background-position: -700px -560px; } +.emoji-pizza { background-position: -700px -580px; } +.emoji-place_of_worship { background-position: -700px -600px; } +.emoji-play_pause { background-position: -700px -620px; } +.emoji-point_down { background-position: -700px -640px; } +.emoji-point_down_tone1 { background-position: -700px -660px; } +.emoji-point_down_tone2 { background-position: -700px -680px; } +.emoji-point_down_tone3 { background-position: 0 -700px; } +.emoji-point_down_tone4 { background-position: -20px -700px; } +.emoji-point_down_tone5 { background-position: -40px -700px; } +.emoji-point_left { background-position: -60px -700px; } +.emoji-point_left_tone1 { background-position: -80px -700px; } +.emoji-point_left_tone2 { background-position: -100px -700px; } +.emoji-point_left_tone3 { background-position: -120px -700px; } +.emoji-point_left_tone4 { background-position: -140px -700px; } +.emoji-point_left_tone5 { background-position: -160px -700px; } +.emoji-point_right { background-position: -180px -700px; } +.emoji-point_right_tone1 { background-position: -200px -700px; } +.emoji-point_right_tone2 { background-position: -220px -700px; } +.emoji-point_right_tone3 { background-position: -240px -700px; } +.emoji-point_right_tone4 { background-position: -260px -700px; } +.emoji-point_right_tone5 { background-position: -280px -700px; } +.emoji-point_up { background-position: -300px -700px; } +.emoji-point_up_2 { background-position: -320px -700px; } +.emoji-point_up_2_tone1 { background-position: -340px -700px; } +.emoji-point_up_2_tone2 { background-position: -360px -700px; } +.emoji-point_up_2_tone3 { background-position: -380px -700px; } +.emoji-point_up_2_tone4 { background-position: -400px -700px; } +.emoji-point_up_2_tone5 { background-position: -420px -700px; } +.emoji-point_up_tone1 { background-position: -440px -700px; } +.emoji-point_up_tone2 { background-position: -460px -700px; } +.emoji-point_up_tone3 { background-position: -480px -700px; } +.emoji-point_up_tone4 { background-position: -500px -700px; } +.emoji-point_up_tone5 { background-position: -520px -700px; } +.emoji-police_car { background-position: -540px -700px; } +.emoji-poodle { background-position: -560px -700px; } +.emoji-poop { background-position: -580px -700px; } +.emoji-popcorn { background-position: -600px -700px; } +.emoji-post_office { background-position: -620px -700px; } +.emoji-postal_horn { background-position: -640px -700px; } +.emoji-postbox { background-position: -660px -700px; } +.emoji-potable_water { background-position: -680px -700px; } +.emoji-potato { background-position: -700px -700px; } +.emoji-pouch { background-position: -720px 0; } +.emoji-poultry_leg { background-position: -720px -20px; } +.emoji-pound { background-position: -720px -40px; } +.emoji-pouting_cat { background-position: -720px -60px; } +.emoji-pray { background-position: -720px -80px; } +.emoji-pray_tone1 { background-position: -720px -100px; } +.emoji-pray_tone2 { background-position: -720px -120px; } +.emoji-pray_tone3 { background-position: -720px -140px; } +.emoji-pray_tone4 { background-position: -720px -160px; } +.emoji-pray_tone5 { background-position: -720px -180px; } +.emoji-prayer_beads { background-position: -720px -200px; } +.emoji-pregnant_woman { background-position: -720px -220px; } +.emoji-pregnant_woman_tone1 { background-position: -720px -240px; } +.emoji-pregnant_woman_tone2 { background-position: -720px -260px; } +.emoji-pregnant_woman_tone3 { background-position: -720px -280px; } +.emoji-pregnant_woman_tone4 { background-position: -720px -300px; } +.emoji-pregnant_woman_tone5 { background-position: -720px -320px; } +.emoji-prince { background-position: -720px -340px; } +.emoji-prince_tone1 { background-position: -720px -360px; } +.emoji-prince_tone2 { background-position: -720px -380px; } +.emoji-prince_tone3 { background-position: -720px -400px; } +.emoji-prince_tone4 { background-position: -720px -420px; } +.emoji-prince_tone5 { background-position: -720px -440px; } +.emoji-princess { background-position: -720px -460px; } +.emoji-princess_tone1 { background-position: -720px -480px; } +.emoji-princess_tone2 { background-position: -720px -500px; } +.emoji-princess_tone3 { background-position: -720px -520px; } +.emoji-princess_tone4 { background-position: -720px -540px; } +.emoji-princess_tone5 { background-position: -720px -560px; } +.emoji-printer { background-position: -720px -580px; } +.emoji-projector { background-position: -720px -600px; } +.emoji-punch { background-position: -720px -620px; } +.emoji-punch_tone1 { background-position: -720px -640px; } +.emoji-punch_tone2 { background-position: -720px -660px; } +.emoji-punch_tone3 { background-position: -720px -680px; } +.emoji-punch_tone4 { background-position: -720px -700px; } +.emoji-punch_tone5 { background-position: 0 -720px; } +.emoji-purple_heart { background-position: -20px -720px; } +.emoji-purse { background-position: -40px -720px; } +.emoji-pushpin { background-position: -60px -720px; } +.emoji-put_litter_in_its_place { background-position: -80px -720px; } +.emoji-question { background-position: -100px -720px; } +.emoji-rabbit { background-position: -120px -720px; } +.emoji-rabbit2 { background-position: -140px -720px; } +.emoji-race_car { background-position: -160px -720px; } +.emoji-racehorse { background-position: -180px -720px; } +.emoji-radio { background-position: -200px -720px; } +.emoji-radio_button { background-position: -220px -720px; } +.emoji-radioactive { background-position: -240px -720px; } +.emoji-rage { background-position: -260px -720px; } +.emoji-railway_car { background-position: -280px -720px; } +.emoji-railway_track { background-position: -300px -720px; } +.emoji-rainbow { background-position: -320px -720px; } +.emoji-raised_back_of_hand { background-position: -340px -720px; } +.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; } +.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; } +.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; } +.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; } +.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; } +.emoji-raised_hand { background-position: -460px -720px; } +.emoji-raised_hand_tone1 { background-position: -480px -720px; } +.emoji-raised_hand_tone2 { background-position: -500px -720px; } +.emoji-raised_hand_tone3 { background-position: -520px -720px; } +.emoji-raised_hand_tone4 { background-position: -540px -720px; } +.emoji-raised_hand_tone5 { background-position: -560px -720px; } +.emoji-raised_hands { background-position: -580px -720px; } +.emoji-raised_hands_tone1 { background-position: -600px -720px; } +.emoji-raised_hands_tone2 { background-position: -620px -720px; } +.emoji-raised_hands_tone3 { background-position: -640px -720px; } +.emoji-raised_hands_tone4 { background-position: -660px -720px; } +.emoji-raised_hands_tone5 { background-position: -680px -720px; } +.emoji-raising_hand { background-position: -700px -720px; } +.emoji-raising_hand_tone1 { background-position: -720px -720px; } +.emoji-raising_hand_tone2 { background-position: -740px 0; } +.emoji-raising_hand_tone3 { background-position: -740px -20px; } +.emoji-raising_hand_tone4 { background-position: -740px -40px; } +.emoji-raising_hand_tone5 { background-position: -740px -60px; } +.emoji-ram { background-position: -740px -80px; } +.emoji-ramen { background-position: -740px -100px; } +.emoji-rat { background-position: -740px -120px; } +.emoji-record_button { background-position: -740px -140px; } +.emoji-recycle { background-position: -740px -160px; } +.emoji-red_car { background-position: -740px -180px; } +.emoji-red_circle { background-position: -740px -200px; } +.emoji-registered { background-position: -740px -220px; } +.emoji-relaxed { background-position: -740px -240px; } +.emoji-relieved { background-position: -740px -260px; } +.emoji-reminder_ribbon { background-position: -740px -280px; } +.emoji-repeat { background-position: -740px -300px; } +.emoji-repeat_one { background-position: -740px -320px; } +.emoji-restroom { background-position: -740px -340px; } +.emoji-revolving_hearts { background-position: -740px -360px; } +.emoji-rewind { background-position: -740px -380px; } +.emoji-rhino { background-position: -740px -400px; } +.emoji-ribbon { background-position: -740px -420px; } +.emoji-rice { background-position: -740px -440px; } +.emoji-rice_ball { background-position: -740px -460px; } +.emoji-rice_cracker { background-position: -740px -480px; } +.emoji-rice_scene { background-position: -740px -500px; } +.emoji-right_facing_fist { background-position: -740px -520px; } +.emoji-right_facing_fist_tone1 { background-position: -740px -540px; } +.emoji-right_facing_fist_tone2 { background-position: -740px -560px; } +.emoji-right_facing_fist_tone3 { background-position: -740px -580px; } +.emoji-right_facing_fist_tone4 { background-position: -740px -600px; } +.emoji-right_facing_fist_tone5 { background-position: -740px -620px; } +.emoji-ring { background-position: -740px -640px; } +.emoji-robot { background-position: -740px -660px; } +.emoji-rocket { background-position: -740px -680px; } +.emoji-rofl { background-position: -740px -700px; } +.emoji-roller_coaster { background-position: -740px -720px; } +.emoji-rolling_eyes { background-position: 0 -740px; } +.emoji-rooster { background-position: -20px -740px; } +.emoji-rose { background-position: -40px -740px; } +.emoji-rosette { background-position: -60px -740px; } +.emoji-rotating_light { background-position: -80px -740px; } +.emoji-round_pushpin { background-position: -100px -740px; } +.emoji-rowboat { background-position: -120px -740px; } +.emoji-rowboat_tone1 { background-position: -140px -740px; } +.emoji-rowboat_tone2 { background-position: -160px -740px; } +.emoji-rowboat_tone3 { background-position: -180px -740px; } +.emoji-rowboat_tone4 { background-position: -200px -740px; } +.emoji-rowboat_tone5 { background-position: -220px -740px; } +.emoji-rugby_football { background-position: -240px -740px; } +.emoji-runner { background-position: -260px -740px; } +.emoji-runner_tone1 { background-position: -280px -740px; } +.emoji-runner_tone2 { background-position: -300px -740px; } +.emoji-runner_tone3 { background-position: -320px -740px; } +.emoji-runner_tone4 { background-position: -340px -740px; } +.emoji-runner_tone5 { background-position: -360px -740px; } +.emoji-running_shirt_with_sash { background-position: -380px -740px; } +.emoji-sa { background-position: -400px -740px; } +.emoji-sagittarius { background-position: -420px -740px; } +.emoji-sailboat { background-position: -440px -740px; } +.emoji-sake { background-position: -460px -740px; } +.emoji-salad { background-position: -480px -740px; } +.emoji-sandal { background-position: -500px -740px; } +.emoji-santa { background-position: -520px -740px; } +.emoji-santa_tone1 { background-position: -540px -740px; } +.emoji-santa_tone2 { background-position: -560px -740px; } +.emoji-santa_tone3 { background-position: -580px -740px; } +.emoji-santa_tone4 { background-position: -600px -740px; } +.emoji-santa_tone5 { background-position: -620px -740px; } +.emoji-satellite { background-position: -640px -740px; } +.emoji-satellite_orbital { background-position: -660px -740px; } +.emoji-saxophone { background-position: -680px -740px; } +.emoji-scales { background-position: -700px -740px; } +.emoji-school { background-position: -720px -740px; } +.emoji-school_satchel { background-position: -740px -740px; } +.emoji-scissors { background-position: -760px 0; } +.emoji-scooter { background-position: -760px -20px; } +.emoji-scorpion { background-position: -760px -40px; } +.emoji-scorpius { background-position: -760px -60px; } +.emoji-scream { background-position: -760px -80px; } +.emoji-scream_cat { background-position: -760px -100px; } +.emoji-scroll { background-position: -760px -120px; } +.emoji-seat { background-position: -760px -140px; } +.emoji-second_place { background-position: -760px -160px; } +.emoji-secret { background-position: -760px -180px; } +.emoji-see_no_evil { background-position: -760px -200px; } +.emoji-seedling { background-position: -760px -220px; } +.emoji-selfie { background-position: -760px -240px; } +.emoji-selfie_tone1 { background-position: -760px -260px; } +.emoji-selfie_tone2 { background-position: -760px -280px; } +.emoji-selfie_tone3 { background-position: -760px -300px; } +.emoji-selfie_tone4 { background-position: -760px -320px; } +.emoji-selfie_tone5 { background-position: -760px -340px; } +.emoji-seven { background-position: -760px -360px; } +.emoji-shallow_pan_of_food { background-position: -760px -380px; } +.emoji-shamrock { background-position: -760px -400px; } +.emoji-shark { background-position: -760px -420px; } +.emoji-shaved_ice { background-position: -760px -440px; } +.emoji-sheep { background-position: -760px -460px; } +.emoji-shell { background-position: -760px -480px; } +.emoji-shield { background-position: -760px -500px; } +.emoji-shinto_shrine { background-position: -760px -520px; } +.emoji-ship { background-position: -760px -540px; } +.emoji-shirt { background-position: -760px -560px; } +.emoji-shopping_bags { background-position: -760px -580px; } +.emoji-shopping_cart { background-position: -760px -600px; } +.emoji-shower { background-position: -760px -620px; } +.emoji-shrimp { background-position: -760px -640px; } +.emoji-shrug { background-position: -760px -660px; } +.emoji-shrug_tone1 { background-position: -760px -680px; } +.emoji-shrug_tone2 { background-position: -760px -700px; } +.emoji-shrug_tone3 { background-position: -760px -720px; } +.emoji-shrug_tone4 { background-position: -760px -740px; } +.emoji-shrug_tone5 { background-position: 0 -760px; } +.emoji-signal_strength { background-position: -20px -760px; } +.emoji-six { background-position: -40px -760px; } +.emoji-six_pointed_star { background-position: -60px -760px; } +.emoji-ski { background-position: -80px -760px; } +.emoji-skier { background-position: -100px -760px; } +.emoji-skull { background-position: -120px -760px; } +.emoji-skull_crossbones { background-position: -140px -760px; } +.emoji-sleeping { background-position: -160px -760px; } +.emoji-sleeping_accommodation { background-position: -180px -760px; } +.emoji-sleepy { background-position: -200px -760px; } +.emoji-slight_frown { background-position: -220px -760px; } +.emoji-slight_smile { background-position: -240px -760px; } +.emoji-slot_machine { background-position: -260px -760px; } +.emoji-small_blue_diamond { background-position: -280px -760px; } +.emoji-small_orange_diamond { background-position: -300px -760px; } +.emoji-small_red_triangle { background-position: -320px -760px; } +.emoji-small_red_triangle_down { background-position: -340px -760px; } +.emoji-smile { background-position: -360px -760px; } +.emoji-smile_cat { background-position: -380px -760px; } +.emoji-smiley { background-position: -400px -760px; } +.emoji-smiley_cat { background-position: -420px -760px; } +.emoji-smiling_imp { background-position: -440px -760px; } +.emoji-smirk { background-position: -460px -760px; } +.emoji-smirk_cat { background-position: -480px -760px; } +.emoji-smoking { background-position: -500px -760px; } +.emoji-snail { background-position: -520px -760px; } +.emoji-snake { background-position: -540px -760px; } +.emoji-sneezing_face { background-position: -560px -760px; } +.emoji-snowboarder { background-position: -580px -760px; } +.emoji-snowflake { background-position: -600px -760px; } +.emoji-snowman { background-position: -620px -760px; } +.emoji-snowman2 { background-position: -640px -760px; } +.emoji-sob { background-position: -660px -760px; } +.emoji-soccer { background-position: -680px -760px; } +.emoji-soon { background-position: -700px -760px; } +.emoji-sos { background-position: -720px -760px; } +.emoji-sound { background-position: -740px -760px; } +.emoji-space_invader { background-position: -760px -760px; } +.emoji-spades { background-position: -780px 0; } +.emoji-spaghetti { background-position: -780px -20px; } +.emoji-sparkle { background-position: -780px -40px; } +.emoji-sparkler { background-position: -780px -60px; } +.emoji-sparkles { background-position: -780px -80px; } +.emoji-sparkling_heart { background-position: -780px -100px; } +.emoji-speak_no_evil { background-position: -780px -120px; } +.emoji-speaker { background-position: -780px -140px; } +.emoji-speaking_head { background-position: -780px -160px; } +.emoji-speech_balloon { background-position: -780px -180px; } +.emoji-speedboat { background-position: -780px -200px; } +.emoji-spider { background-position: -780px -220px; } +.emoji-spider_web { background-position: -780px -240px; } +.emoji-spoon { background-position: -780px -260px; } +.emoji-spy { background-position: -780px -280px; } +.emoji-spy_tone1 { background-position: -780px -300px; } +.emoji-spy_tone2 { background-position: -780px -320px; } +.emoji-spy_tone3 { background-position: -780px -340px; } +.emoji-spy_tone4 { background-position: -780px -360px; } +.emoji-spy_tone5 { background-position: -780px -380px; } +.emoji-squid { background-position: -780px -400px; } +.emoji-stadium { background-position: -780px -420px; } +.emoji-star { background-position: -780px -440px; } +.emoji-star2 { background-position: -780px -460px; } +.emoji-star_and_crescent { background-position: -780px -480px; } +.emoji-star_of_david { background-position: -780px -500px; } +.emoji-stars { background-position: -780px -520px; } +.emoji-station { background-position: -780px -540px; } +.emoji-statue_of_liberty { background-position: -780px -560px; } +.emoji-steam_locomotive { background-position: -780px -580px; } +.emoji-stew { background-position: -780px -600px; } +.emoji-stop_button { background-position: -780px -620px; } +.emoji-stopwatch { background-position: -780px -640px; } +.emoji-straight_ruler { background-position: -780px -660px; } +.emoji-strawberry { background-position: -780px -680px; } +.emoji-stuck_out_tongue { background-position: -780px -700px; } +.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; } +.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; } +.emoji-stuffed_flatbread { background-position: -780px -760px; } +.emoji-sun_with_face { background-position: 0 -780px; } +.emoji-sunflower { background-position: -20px -780px; } +.emoji-sunglasses { background-position: -40px -780px; } +.emoji-sunny { background-position: -60px -780px; } +.emoji-sunrise { background-position: -80px -780px; } +.emoji-sunrise_over_mountains { background-position: -100px -780px; } +.emoji-surfer { background-position: -120px -780px; } +.emoji-surfer_tone1 { background-position: -140px -780px; } +.emoji-surfer_tone2 { background-position: -160px -780px; } +.emoji-surfer_tone3 { background-position: -180px -780px; } +.emoji-surfer_tone4 { background-position: -200px -780px; } +.emoji-surfer_tone5 { background-position: -220px -780px; } +.emoji-sushi { background-position: -240px -780px; } +.emoji-suspension_railway { background-position: -260px -780px; } +.emoji-sweat { background-position: -280px -780px; } +.emoji-sweat_drops { background-position: -300px -780px; } +.emoji-sweat_smile { background-position: -320px -780px; } +.emoji-sweet_potato { background-position: -340px -780px; } +.emoji-swimmer { background-position: -360px -780px; } +.emoji-swimmer_tone1 { background-position: -380px -780px; } +.emoji-swimmer_tone2 { background-position: -400px -780px; } +.emoji-swimmer_tone3 { background-position: -420px -780px; } +.emoji-swimmer_tone4 { background-position: -440px -780px; } +.emoji-swimmer_tone5 { background-position: -460px -780px; } +.emoji-symbols { background-position: -480px -780px; } +.emoji-synagogue { background-position: -500px -780px; } +.emoji-syringe { background-position: -520px -780px; } +.emoji-taco { background-position: -540px -780px; } +.emoji-tada { background-position: -560px -780px; } +.emoji-tanabata_tree { background-position: -580px -780px; } +.emoji-tangerine { background-position: -600px -780px; } +.emoji-taurus { background-position: -620px -780px; } +.emoji-taxi { background-position: -640px -780px; } +.emoji-tea { background-position: -660px -780px; } +.emoji-telephone { background-position: -680px -780px; } +.emoji-telephone_receiver { background-position: -700px -780px; } +.emoji-telescope { background-position: -720px -780px; } +.emoji-ten { background-position: -740px -780px; } +.emoji-tennis { background-position: -760px -780px; } +.emoji-tent { background-position: -780px -780px; } +.emoji-thermometer { background-position: -800px 0; } +.emoji-thermometer_face { background-position: -800px -20px; } +.emoji-thinking { background-position: -800px -40px; } +.emoji-third_place { background-position: -800px -60px; } +.emoji-thought_balloon { background-position: -800px -80px; } +.emoji-three { background-position: -800px -100px; } +.emoji-thumbsdown { background-position: -800px -120px; } +.emoji-thumbsdown_tone1 { background-position: -800px -140px; } +.emoji-thumbsdown_tone2 { background-position: -800px -160px; } +.emoji-thumbsdown_tone3 { background-position: -800px -180px; } +.emoji-thumbsdown_tone4 { background-position: -800px -200px; } +.emoji-thumbsdown_tone5 { background-position: -800px -220px; } +.emoji-thumbsup { background-position: -800px -240px; } +.emoji-thumbsup_tone1 { background-position: -800px -260px; } +.emoji-thumbsup_tone2 { background-position: -800px -280px; } +.emoji-thumbsup_tone3 { background-position: -800px -300px; } +.emoji-thumbsup_tone4 { background-position: -800px -320px; } +.emoji-thumbsup_tone5 { background-position: -800px -340px; } +.emoji-thunder_cloud_rain { background-position: -800px -360px; } +.emoji-ticket { background-position: -800px -380px; } +.emoji-tickets { background-position: -800px -400px; } +.emoji-tiger { background-position: -800px -420px; } +.emoji-tiger2 { background-position: -800px -440px; } +.emoji-timer { background-position: -800px -460px; } +.emoji-tired_face { background-position: -800px -480px; } +.emoji-tm { background-position: -800px -500px; } +.emoji-toilet { background-position: -800px -520px; } +.emoji-tokyo_tower { background-position: -800px -540px; } +.emoji-tomato { background-position: -800px -560px; } +.emoji-tone1 { background-position: -800px -580px; } +.emoji-tone2 { background-position: -800px -600px; } +.emoji-tone3 { background-position: -800px -620px; } +.emoji-tone4 { background-position: -800px -640px; } +.emoji-tone5 { background-position: -800px -660px; } +.emoji-tongue { background-position: -800px -680px; } +.emoji-tools { background-position: -800px -700px; } +.emoji-top { background-position: -800px -720px; } +.emoji-tophat { background-position: -800px -740px; } +.emoji-track_next { background-position: -800px -760px; } +.emoji-track_previous { background-position: -800px -780px; } +.emoji-trackball { background-position: 0 -800px; } +.emoji-tractor { background-position: -20px -800px; } +.emoji-traffic_light { background-position: -40px -800px; } +.emoji-train { background-position: -60px -800px; } +.emoji-train2 { background-position: -80px -800px; } +.emoji-tram { background-position: -100px -800px; } +.emoji-triangular_flag_on_post { background-position: -120px -800px; } +.emoji-triangular_ruler { background-position: -140px -800px; } +.emoji-trident { background-position: -160px -800px; } +.emoji-triumph { background-position: -180px -800px; } +.emoji-trolleybus { background-position: -200px -800px; } +.emoji-trophy { background-position: -220px -800px; } +.emoji-tropical_drink { background-position: -240px -800px; } +.emoji-tropical_fish { background-position: -260px -800px; } +.emoji-truck { background-position: -280px -800px; } +.emoji-trumpet { background-position: -300px -800px; } +.emoji-tulip { background-position: -320px -800px; } +.emoji-tumbler_glass { background-position: -340px -800px; } +.emoji-turkey { background-position: -360px -800px; } +.emoji-turtle { background-position: -380px -800px; } +.emoji-tv { background-position: -400px -800px; } +.emoji-twisted_rightwards_arrows { background-position: -420px -800px; } +.emoji-two { background-position: -440px -800px; } +.emoji-two_hearts { background-position: -460px -800px; } +.emoji-two_men_holding_hands { background-position: -480px -800px; } +.emoji-two_women_holding_hands { background-position: -500px -800px; } +.emoji-u5272 { background-position: -520px -800px; } +.emoji-u5408 { background-position: -540px -800px; } +.emoji-u55b6 { background-position: -560px -800px; } +.emoji-u6307 { background-position: -580px -800px; } +.emoji-u6708 { background-position: -600px -800px; } +.emoji-u6709 { background-position: -620px -800px; } +.emoji-u6e80 { background-position: -640px -800px; } +.emoji-u7121 { background-position: -660px -800px; } +.emoji-u7533 { background-position: -680px -800px; } +.emoji-u7981 { background-position: -700px -800px; } +.emoji-u7a7a { background-position: -720px -800px; } +.emoji-umbrella { background-position: -740px -800px; } +.emoji-umbrella2 { background-position: -760px -800px; } +.emoji-unamused { background-position: -780px -800px; } +.emoji-underage { background-position: -800px -800px; } +.emoji-unicorn { background-position: -820px 0; } +.emoji-unlock { background-position: -820px -20px; } +.emoji-up { background-position: -820px -40px; } +.emoji-upside_down { background-position: -820px -60px; } +.emoji-urn { background-position: -820px -80px; } +.emoji-v { background-position: -820px -100px; } +.emoji-v_tone1 { background-position: -820px -120px; } +.emoji-v_tone2 { background-position: -820px -140px; } +.emoji-v_tone3 { background-position: -820px -160px; } +.emoji-v_tone4 { background-position: -820px -180px; } +.emoji-v_tone5 { background-position: -820px -200px; } +.emoji-vertical_traffic_light { background-position: -820px -220px; } +.emoji-vhs { background-position: -820px -240px; } +.emoji-vibration_mode { background-position: -820px -260px; } +.emoji-video_camera { background-position: -820px -280px; } +.emoji-video_game { background-position: -820px -300px; } +.emoji-violin { background-position: -820px -320px; } +.emoji-virgo { background-position: -820px -340px; } +.emoji-volcano { background-position: -820px -360px; } +.emoji-volleyball { background-position: -820px -380px; } +.emoji-vs { background-position: -820px -400px; } +.emoji-vulcan { background-position: -820px -420px; } +.emoji-vulcan_tone1 { background-position: -820px -440px; } +.emoji-vulcan_tone2 { background-position: -820px -460px; } +.emoji-vulcan_tone3 { background-position: -820px -480px; } +.emoji-vulcan_tone4 { background-position: -820px -500px; } +.emoji-vulcan_tone5 { background-position: -820px -520px; } +.emoji-walking { background-position: -820px -540px; } +.emoji-walking_tone1 { background-position: -820px -560px; } +.emoji-walking_tone2 { background-position: -820px -580px; } +.emoji-walking_tone3 { background-position: -820px -600px; } +.emoji-walking_tone4 { background-position: -820px -620px; } +.emoji-walking_tone5 { background-position: -820px -640px; } +.emoji-waning_crescent_moon { background-position: -820px -660px; } +.emoji-waning_gibbous_moon { background-position: -820px -680px; } +.emoji-warning { background-position: -820px -700px; } +.emoji-wastebasket { background-position: -820px -720px; } +.emoji-watch { background-position: -820px -740px; } +.emoji-water_buffalo { background-position: -820px -760px; } +.emoji-water_polo { background-position: -820px -780px; } +.emoji-water_polo_tone1 { background-position: -820px -800px; } +.emoji-water_polo_tone2 { background-position: 0 -820px; } +.emoji-water_polo_tone3 { background-position: -20px -820px; } +.emoji-water_polo_tone4 { background-position: -40px -820px; } +.emoji-water_polo_tone5 { background-position: -60px -820px; } +.emoji-watermelon { background-position: -80px -820px; } +.emoji-wave { background-position: -100px -820px; } +.emoji-wave_tone1 { background-position: -120px -820px; } +.emoji-wave_tone2 { background-position: -140px -820px; } +.emoji-wave_tone3 { background-position: -160px -820px; } +.emoji-wave_tone4 { background-position: -180px -820px; } +.emoji-wave_tone5 { background-position: -200px -820px; } +.emoji-wavy_dash { background-position: -220px -820px; } +.emoji-waxing_crescent_moon { background-position: -240px -820px; } +.emoji-waxing_gibbous_moon { background-position: -260px -820px; } +.emoji-wc { background-position: -280px -820px; } +.emoji-weary { background-position: -300px -820px; } +.emoji-wedding { background-position: -320px -820px; } +.emoji-whale { background-position: -340px -820px; } +.emoji-whale2 { background-position: -360px -820px; } +.emoji-wheel_of_dharma { background-position: -380px -820px; } +.emoji-wheelchair { background-position: -400px -820px; } +.emoji-white_check_mark { background-position: -420px -820px; } +.emoji-white_circle { background-position: -440px -820px; } +.emoji-white_flower { background-position: -460px -820px; } +.emoji-white_large_square { background-position: -480px -820px; } +.emoji-white_medium_small_square { background-position: -500px -820px; } +.emoji-white_medium_square { background-position: -520px -820px; } +.emoji-white_small_square { background-position: -540px -820px; } +.emoji-white_square_button { background-position: -560px -820px; } +.emoji-white_sun_cloud { background-position: -580px -820px; } +.emoji-white_sun_rain_cloud { background-position: -600px -820px; } +.emoji-white_sun_small_cloud { background-position: -620px -820px; } +.emoji-wilted_rose { background-position: -640px -820px; } +.emoji-wind_blowing_face { background-position: -660px -820px; } +.emoji-wind_chime { background-position: -680px -820px; } +.emoji-wine_glass { background-position: -700px -820px; } +.emoji-wink { background-position: -720px -820px; } +.emoji-wolf { background-position: -740px -820px; } +.emoji-woman { background-position: -760px -820px; } +.emoji-woman_tone1 { background-position: -780px -820px; } +.emoji-woman_tone2 { background-position: -800px -820px; } +.emoji-woman_tone3 { background-position: -820px -820px; } +.emoji-woman_tone4 { background-position: -840px 0; } +.emoji-woman_tone5 { background-position: -840px -20px; } +.emoji-womans_clothes { background-position: -840px -40px; } +.emoji-womans_hat { background-position: -840px -60px; } +.emoji-womens { background-position: -840px -80px; } +.emoji-worried { background-position: -840px -100px; } +.emoji-wrench { background-position: -840px -120px; } +.emoji-wrestlers { background-position: -840px -140px; } +.emoji-wrestlers_tone1 { background-position: -840px -160px; } +.emoji-wrestlers_tone2 { background-position: -840px -180px; } +.emoji-wrestlers_tone3 { background-position: -840px -200px; } +.emoji-wrestlers_tone4 { background-position: -840px -220px; } +.emoji-wrestlers_tone5 { background-position: -840px -240px; } +.emoji-writing_hand { background-position: -840px -260px; } +.emoji-writing_hand_tone1 { background-position: -840px -280px; } +.emoji-writing_hand_tone2 { background-position: -840px -300px; } +.emoji-writing_hand_tone3 { background-position: -840px -320px; } +.emoji-writing_hand_tone4 { background-position: -840px -340px; } +.emoji-writing_hand_tone5 { background-position: -840px -360px; } +.emoji-x { background-position: -840px -380px; } +.emoji-yellow_heart { background-position: -840px -400px; } +.emoji-yen { background-position: -840px -420px; } +.emoji-yin_yang { background-position: -840px -440px; } +.emoji-yum { background-position: -840px -460px; } +.emoji-zap { background-position: -840px -480px; } +.emoji-zero { background-position: -840px -500px; } +.emoji-zipper_mouth { background-position: -840px -520px; } +.emoji-100 { background-position: -840px -540px; } + +.emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + color: transparent; + text-indent: -99em; + height: 20px; + width: 20px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 860px 840px; + } +} diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 7158de65143..d86ae57cd9a 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,1809 +1,7 @@ -.emoji-0023-20E3 { background-position: 0 0; } -.emoji-002A-20E3 { background-position: -20px 0; } -.emoji-0030-20E3 { background-position: 0 -20px; } -.emoji-0031-20E3 { background-position: -20px -20px; } -.emoji-0032-20E3 { background-position: -40px 0; } -.emoji-0033-20E3 { background-position: -40px -20px; } -.emoji-0034-20E3 { background-position: 0 -40px; } -.emoji-0035-20E3 { background-position: -20px -40px; } -.emoji-0036-20E3 { background-position: -40px -40px; } -.emoji-0037-20E3 { background-position: -60px 0; } -.emoji-0038-20E3 { background-position: -60px -20px; } -.emoji-0039-20E3 { background-position: -60px -40px; } -.emoji-00A9 { background-position: 0 -60px; } -.emoji-00AE { background-position: -20px -60px; } -.emoji-1F004 { background-position: -40px -60px; } -.emoji-1F0CF { background-position: -60px -60px; } -.emoji-1F170 { background-position: -80px 0; } -.emoji-1F171 { background-position: -80px -20px; } -.emoji-1F17E { background-position: -80px -40px; } -.emoji-1F17F { background-position: -80px -60px; } -.emoji-1F18E { background-position: 0 -80px; } -.emoji-1F191 { background-position: -20px -80px; } -.emoji-1F192 { background-position: -40px -80px; } -.emoji-1F193 { background-position: -60px -80px; } -.emoji-1F194 { background-position: -80px -80px; } -.emoji-1F195 { background-position: -100px 0; } -.emoji-1F196 { background-position: -100px -20px; } -.emoji-1F197 { background-position: -100px -40px; } -.emoji-1F198 { background-position: -100px -60px; } -.emoji-1F199 { background-position: -100px -80px; } -.emoji-1F19A { background-position: 0 -100px; } -.emoji-1F1E6-1F1E8 { background-position: -20px -100px; } -.emoji-1F1E6-1F1E9 { background-position: -40px -100px; } -.emoji-1F1E6-1F1EA { background-position: -60px -100px; } -.emoji-1F1E6-1F1EB { background-position: -80px -100px; } -.emoji-1F1E6-1F1EC { background-position: -100px -100px; } -.emoji-1F1E6-1F1EE { background-position: -120px 0; } -.emoji-1F1E6-1F1F1 { background-position: -120px -20px; } -.emoji-1F1E6-1F1F2 { background-position: -120px -40px; } -.emoji-1F1E6-1F1F4 { background-position: -120px -60px; } -.emoji-1F1E6-1F1F6 { background-position: -120px -80px; } -.emoji-1F1E6-1F1F7 { background-position: -120px -100px; } -.emoji-1F1E6-1F1F8 { background-position: 0 -120px; } -.emoji-1F1E6-1F1F9 { background-position: -20px -120px; } -.emoji-1F1E6-1F1FA { background-position: -40px -120px; } -.emoji-1F1E6-1F1FC { background-position: -60px -120px; } -.emoji-1F1E6-1F1FD { background-position: -80px -120px; } -.emoji-1F1E6-1F1FF { background-position: -100px -120px; } -.emoji-1F1E7-1F1E6 { background-position: -120px -120px; } -.emoji-1F1E7-1F1E7 { background-position: -140px 0; } -.emoji-1F1E7-1F1E9 { background-position: -140px -20px; } -.emoji-1F1E7-1F1EA { background-position: -140px -40px; } -.emoji-1F1E7-1F1EB { background-position: -140px -60px; } -.emoji-1F1E7-1F1EC { background-position: -140px -80px; } -.emoji-1F1E7-1F1ED { background-position: -140px -100px; } -.emoji-1F1E7-1F1EE { background-position: -140px -120px; } -.emoji-1F1E7-1F1EF { background-position: 0 -140px; } -.emoji-1F1E7-1F1F1 { background-position: -20px -140px; } -.emoji-1F1E7-1F1F2 { background-position: -40px -140px; } -.emoji-1F1E7-1F1F3 { background-position: -60px -140px; } -.emoji-1F1E7-1F1F4 { background-position: -80px -140px; } -.emoji-1F1E7-1F1F6 { background-position: -100px -140px; } -.emoji-1F1E7-1F1F7 { background-position: -120px -140px; } -.emoji-1F1E7-1F1F8 { background-position: -140px -140px; } -.emoji-1F1E7-1F1F9 { background-position: -160px 0; } -.emoji-1F1E7-1F1FB { background-position: -160px -20px; } -.emoji-1F1E7-1F1FC { background-position: -160px -40px; } -.emoji-1F1E7-1F1FE { background-position: -160px -60px; } -.emoji-1F1E7-1F1FF { background-position: -160px -80px; } -.emoji-1F1E8-1F1E6 { background-position: -160px -100px; } -.emoji-1F1E8-1F1E8 { background-position: -160px -120px; } -.emoji-1F1E8-1F1E9 { background-position: -160px -140px; } -.emoji-1F1E8-1F1EB { background-position: 0 -160px; } -.emoji-1F1E8-1F1EC { background-position: -20px -160px; } -.emoji-1F1E8-1F1ED { background-position: -40px -160px; } -.emoji-1F1E8-1F1EE { background-position: -60px -160px; } -.emoji-1F1E8-1F1F0 { background-position: -80px -160px; } -.emoji-1F1E8-1F1F1 { background-position: -100px -160px; } -.emoji-1F1E8-1F1F2 { background-position: -120px -160px; } -.emoji-1F1E8-1F1F3 { background-position: -140px -160px; } -.emoji-1F1E8-1F1F4 { background-position: -160px -160px; } -.emoji-1F1E8-1F1F5 { background-position: -180px 0; } -.emoji-1F1E8-1F1F7 { background-position: -180px -20px; } -.emoji-1F1E8-1F1FA { background-position: -180px -40px; } -.emoji-1F1E8-1F1FB { background-position: -180px -60px; } -.emoji-1F1E8-1F1FC { background-position: -180px -80px; } -.emoji-1F1E8-1F1FD { background-position: -180px -100px; } -.emoji-1F1E8-1F1FE { background-position: -180px -120px; } -.emoji-1F1E8-1F1FF { background-position: -180px -140px; } -.emoji-1F1E9-1F1EA { background-position: -180px -160px; } -.emoji-1F1E9-1F1EC { background-position: 0 -180px; } -.emoji-1F1E9-1F1EF { background-position: -20px -180px; } -.emoji-1F1E9-1F1F0 { background-position: -40px -180px; } -.emoji-1F1E9-1F1F2 { background-position: -60px -180px; } -.emoji-1F1E9-1F1F4 { background-position: -80px -180px; } -.emoji-1F1E9-1F1FF { background-position: -100px -180px; } -.emoji-1F1EA-1F1E6 { background-position: -120px -180px; } -.emoji-1F1EA-1F1E8 { background-position: -140px -180px; } -.emoji-1F1EA-1F1EA { background-position: -160px -180px; } -.emoji-1F1EA-1F1EC { background-position: -180px -180px; } -.emoji-1F1EA-1F1ED { background-position: -200px 0; } -.emoji-1F1EA-1F1F7 { background-position: -200px -20px; } -.emoji-1F1EA-1F1F8 { background-position: -200px -40px; } -.emoji-1F1EA-1F1F9 { background-position: -200px -60px; } -.emoji-1F1EA-1F1FA { background-position: -200px -80px; } -.emoji-1F1EB-1F1EE { background-position: -200px -100px; } -.emoji-1F1EB-1F1EF { background-position: -200px -120px; } -.emoji-1F1EB-1F1F0 { background-position: -200px -140px; } -.emoji-1F1EB-1F1F2 { background-position: -200px -160px; } -.emoji-1F1EB-1F1F4 { background-position: -200px -180px; } -.emoji-1F1EB-1F1F7 { background-position: 0 -200px; } -.emoji-1F1EC-1F1E6 { background-position: -20px -200px; } -.emoji-1F1EC-1F1E7 { background-position: -40px -200px; } -.emoji-1F1EC-1F1E9 { background-position: -60px -200px; } -.emoji-1F1EC-1F1EA { background-position: -80px -200px; } -.emoji-1F1EC-1F1EB { background-position: -100px -200px; } -.emoji-1F1EC-1F1EC { background-position: -120px -200px; } -.emoji-1F1EC-1F1ED { background-position: -140px -200px; } -.emoji-1F1EC-1F1EE { background-position: -160px -200px; } -.emoji-1F1EC-1F1F1 { background-position: -180px -200px; } -.emoji-1F1EC-1F1F2 { background-position: -200px -200px; } -.emoji-1F1EC-1F1F3 { background-position: -220px 0; } -.emoji-1F1EC-1F1F5 { background-position: -220px -20px; } -.emoji-1F1EC-1F1F6 { background-position: -220px -40px; } -.emoji-1F1EC-1F1F7 { background-position: -220px -60px; } -.emoji-1F1EC-1F1F8 { background-position: -220px -80px; } -.emoji-1F1EC-1F1F9 { background-position: -220px -100px; } -.emoji-1F1EC-1F1FA { background-position: -220px -120px; } -.emoji-1F1EC-1F1FC { background-position: -220px -140px; } -.emoji-1F1EC-1F1FE { background-position: -220px -160px; } -.emoji-1F1ED-1F1F0 { background-position: -220px -180px; } -.emoji-1F1ED-1F1F2 { background-position: -220px -200px; } -.emoji-1F1ED-1F1F3 { background-position: 0 -220px; } -.emoji-1F1ED-1F1F7 { background-position: -20px -220px; } -.emoji-1F1ED-1F1F9 { background-position: -40px -220px; } -.emoji-1F1ED-1F1FA { background-position: -60px -220px; } -.emoji-1F1EE-1F1E8 { background-position: -80px -220px; } -.emoji-1F1EE-1F1E9 { background-position: -100px -220px; } -.emoji-1F1EE-1F1EA { background-position: -120px -220px; } -.emoji-1F1EE-1F1F1 { background-position: -140px -220px; } -.emoji-1F1EE-1F1F2 { background-position: -160px -220px; } -.emoji-1F1EE-1F1F3 { background-position: -180px -220px; } -.emoji-1F1EE-1F1F4 { background-position: -200px -220px; } -.emoji-1F1EE-1F1F6 { background-position: -220px -220px; } -.emoji-1F1EE-1F1F7 { background-position: -240px 0; } -.emoji-1F1EE-1F1F8 { background-position: -240px -20px; } -.emoji-1F1EE-1F1F9 { background-position: -240px -40px; } -.emoji-1F1EF-1F1EA { background-position: -240px -60px; } -.emoji-1F1EF-1F1F2 { background-position: -240px -80px; } -.emoji-1F1EF-1F1F4 { background-position: -240px -100px; } -.emoji-1F1EF-1F1F5 { background-position: -240px -120px; } -.emoji-1F1F0-1F1EA { background-position: -240px -140px; } -.emoji-1F1F0-1F1EC { background-position: -240px -160px; } -.emoji-1F1F0-1F1ED { background-position: -240px -180px; } -.emoji-1F1F0-1F1EE { background-position: -240px -200px; } -.emoji-1F1F0-1F1F2 { background-position: -240px -220px; } -.emoji-1F1F0-1F1F3 { background-position: 0 -240px; } -.emoji-1F1F0-1F1F5 { background-position: -20px -240px; } -.emoji-1F1F0-1F1F7 { background-position: -40px -240px; } -.emoji-1F1F0-1F1FC { background-position: -60px -240px; } -.emoji-1F1F0-1F1FE { background-position: -80px -240px; } -.emoji-1F1F0-1F1FF { background-position: -100px -240px; } -.emoji-1F1F1-1F1E6 { background-position: -120px -240px; } -.emoji-1F1F1-1F1E7 { background-position: -140px -240px; } -.emoji-1F1F1-1F1E8 { background-position: -160px -240px; } -.emoji-1F1F1-1F1EE { background-position: -180px -240px; } -.emoji-1F1F1-1F1F0 { background-position: -200px -240px; } -.emoji-1F1F1-1F1F7 { background-position: -220px -240px; } -.emoji-1F1F1-1F1F8 { background-position: -240px -240px; } -.emoji-1F1F1-1F1F9 { background-position: -260px 0; } -.emoji-1F1F1-1F1FA { background-position: -260px -20px; } -.emoji-1F1F1-1F1FB { background-position: -260px -40px; } -.emoji-1F1F1-1F1FE { background-position: -260px -60px; } -.emoji-1F1F2-1F1E6 { background-position: -260px -80px; } -.emoji-1F1F2-1F1E8 { background-position: -260px -100px; } -.emoji-1F1F2-1F1E9 { background-position: -260px -120px; } -.emoji-1F1F2-1F1EA { background-position: -260px -140px; } -.emoji-1F1F2-1F1EB { background-position: -260px -160px; } -.emoji-1F1F2-1F1EC { background-position: -260px -180px; } -.emoji-1F1F2-1F1ED { background-position: -260px -200px; } -.emoji-1F1F2-1F1F0 { background-position: -260px -220px; } -.emoji-1F1F2-1F1F1 { background-position: -260px -240px; } -.emoji-1F1F2-1F1F2 { background-position: 0 -260px; } -.emoji-1F1F2-1F1F3 { background-position: -20px -260px; } -.emoji-1F1F2-1F1F4 { background-position: -40px -260px; } -.emoji-1F1F2-1F1F5 { background-position: -60px -260px; } -.emoji-1F1F2-1F1F6 { background-position: -80px -260px; } -.emoji-1F1F2-1F1F7 { background-position: -100px -260px; } -.emoji-1F1F2-1F1F8 { background-position: -120px -260px; } -.emoji-1F1F2-1F1F9 { background-position: -140px -260px; } -.emoji-1F1F2-1F1FA { background-position: -160px -260px; } -.emoji-1F1F2-1F1FB { background-position: -180px -260px; } -.emoji-1F1F2-1F1FC { background-position: -200px -260px; } -.emoji-1F1F2-1F1FD { background-position: -220px -260px; } -.emoji-1F1F2-1F1FE { background-position: -240px -260px; } -.emoji-1F1F2-1F1FF { background-position: -260px -260px; } -.emoji-1F1F3-1F1E6 { background-position: -280px 0; } -.emoji-1F1F3-1F1E8 { background-position: -280px -20px; } -.emoji-1F1F3-1F1EA { background-position: -280px -40px; } -.emoji-1F1F3-1F1EB { background-position: -280px -60px; } -.emoji-1F1F3-1F1EC { background-position: -280px -80px; } -.emoji-1F1F3-1F1EE { background-position: -280px -100px; } -.emoji-1F1F3-1F1F1 { background-position: -280px -120px; } -.emoji-1F1F3-1F1F4 { background-position: -280px -140px; } -.emoji-1F1F3-1F1F5 { background-position: -280px -160px; } -.emoji-1F1F3-1F1F7 { background-position: -280px -180px; } -.emoji-1F1F3-1F1FA { background-position: -280px -200px; } -.emoji-1F1F3-1F1FF { background-position: -280px -220px; } -.emoji-1F1F4-1F1F2 { background-position: -280px -240px; } -.emoji-1F1F5-1F1E6 { background-position: -280px -260px; } -.emoji-1F1F5-1F1EA { background-position: 0 -280px; } -.emoji-1F1F5-1F1EB { background-position: -20px -280px; } -.emoji-1F1F5-1F1EC { background-position: -40px -280px; } -.emoji-1F1F5-1F1ED { background-position: -60px -280px; } -.emoji-1F1F5-1F1F0 { background-position: -80px -280px; } -.emoji-1F1F5-1F1F1 { background-position: -100px -280px; } -.emoji-1F1F5-1F1F2 { background-position: -120px -280px; } -.emoji-1F1F5-1F1F3 { background-position: -140px -280px; } -.emoji-1F1F5-1F1F7 { background-position: -160px -280px; } -.emoji-1F1F5-1F1F8 { background-position: -180px -280px; } -.emoji-1F1F5-1F1F9 { background-position: -200px -280px; } -.emoji-1F1F5-1F1FC { background-position: -220px -280px; } -.emoji-1F1F5-1F1FE { background-position: -240px -280px; } -.emoji-1F1F6-1F1E6 { background-position: -260px -280px; } -.emoji-1F1F7-1F1EA { background-position: -280px -280px; } -.emoji-1F1F7-1F1F4 { background-position: -300px 0; } -.emoji-1F1F7-1F1F8 { background-position: -300px -20px; } -.emoji-1F1F7-1F1FA { background-position: -300px -40px; } -.emoji-1F1F7-1F1FC { background-position: -300px -60px; } -.emoji-1F1F8-1F1E6 { background-position: -300px -80px; } -.emoji-1F1F8-1F1E7 { background-position: -300px -100px; } -.emoji-1F1F8-1F1E8 { background-position: -300px -120px; } -.emoji-1F1F8-1F1E9 { background-position: -300px -140px; } -.emoji-1F1F8-1F1EA { background-position: -300px -160px; } -.emoji-1F1F8-1F1EC { background-position: -300px -180px; } -.emoji-1F1F8-1F1ED { background-position: -300px -200px; } -.emoji-1F1F8-1F1EE { background-position: -300px -220px; } -.emoji-1F1F8-1F1EF { background-position: -300px -240px; } -.emoji-1F1F8-1F1F0 { background-position: -300px -260px; } -.emoji-1F1F8-1F1F1 { background-position: -300px -280px; } -.emoji-1F1F8-1F1F2 { background-position: 0 -300px; } -.emoji-1F1F8-1F1F3 { background-position: -20px -300px; } -.emoji-1F1F8-1F1F4 { background-position: -40px -300px; } -.emoji-1F1F8-1F1F7 { background-position: -60px -300px; } -.emoji-1F1F8-1F1F8 { background-position: -80px -300px; } -.emoji-1F1F8-1F1F9 { background-position: -100px -300px; } -.emoji-1F1F8-1F1FB { background-position: -120px -300px; } -.emoji-1F1F8-1F1FD { background-position: -140px -300px; } -.emoji-1F1F8-1F1FE { background-position: -160px -300px; } -.emoji-1F1F8-1F1FF { background-position: -180px -300px; } -.emoji-1F1F9-1F1E6 { background-position: -200px -300px; } -.emoji-1F1F9-1F1E8 { background-position: -220px -300px; } -.emoji-1F1F9-1F1E9 { background-position: -240px -300px; } -.emoji-1F1F9-1F1EB { background-position: -260px -300px; } -.emoji-1F1F9-1F1EC { background-position: -280px -300px; } -.emoji-1F1F9-1F1ED { background-position: -300px -300px; } -.emoji-1F1F9-1F1EF { background-position: -320px 0; } -.emoji-1F1F9-1F1F0 { background-position: -320px -20px; } -.emoji-1F1F9-1F1F1 { background-position: -320px -40px; } -.emoji-1F1F9-1F1F2 { background-position: -320px -60px; } -.emoji-1F1F9-1F1F3 { background-position: -320px -80px; } -.emoji-1F1F9-1F1F4 { background-position: -320px -100px; } -.emoji-1F1F9-1F1F7 { background-position: -320px -120px; } -.emoji-1F1F9-1F1F9 { background-position: -320px -140px; } -.emoji-1F1F9-1F1FB { background-position: -320px -160px; } -.emoji-1F1F9-1F1FC { background-position: -320px -180px; } -.emoji-1F1F9-1F1FF { background-position: -320px -200px; } -.emoji-1F1FA-1F1E6 { background-position: -320px -220px; } -.emoji-1F1FA-1F1EC { background-position: -320px -240px; } -.emoji-1F1FA-1F1F2 { background-position: -320px -260px; } -.emoji-1F1FA-1F1F8 { background-position: -320px -280px; } -.emoji-1F1FA-1F1FE { background-position: -320px -300px; } -.emoji-1F1FA-1F1FF { background-position: 0 -320px; } -.emoji-1F1FB-1F1E6 { background-position: -20px -320px; } -.emoji-1F1FB-1F1E8 { background-position: -40px -320px; } -.emoji-1F1FB-1F1EA { background-position: -60px -320px; } -.emoji-1F1FB-1F1EC { background-position: -80px -320px; } -.emoji-1F1FB-1F1EE { background-position: -100px -320px; } -.emoji-1F1FB-1F1F3 { background-position: -120px -320px; } -.emoji-1F1FB-1F1FA { background-position: -140px -320px; } -.emoji-1F1FC-1F1EB { background-position: -160px -320px; } -.emoji-1F1FC-1F1F8 { background-position: -180px -320px; } -.emoji-1F1FD-1F1F0 { background-position: -200px -320px; } -.emoji-1F1FE-1F1EA { background-position: -220px -320px; } -.emoji-1F1FE-1F1F9 { background-position: -240px -320px; } -.emoji-1F1FF-1F1E6 { background-position: -260px -320px; } -.emoji-1F1FF-1F1F2 { background-position: -280px -320px; } -.emoji-1F1FF-1F1FC { background-position: -300px -320px; } -.emoji-1F201 { background-position: -320px -320px; } -.emoji-1F202 { background-position: -340px 0; } -.emoji-1F21A { background-position: -340px -20px; } -.emoji-1F22F { background-position: -340px -40px; } -.emoji-1F232 { background-position: -340px -60px; } -.emoji-1F233 { background-position: -340px -80px; } -.emoji-1F234 { background-position: -340px -100px; } -.emoji-1F235 { background-position: -340px -120px; } -.emoji-1F236 { background-position: -340px -140px; } -.emoji-1F237 { background-position: -340px -160px; } -.emoji-1F238 { background-position: -340px -180px; } -.emoji-1F239 { background-position: -340px -200px; } -.emoji-1F23A { background-position: -340px -220px; } -.emoji-1F250 { background-position: -340px -240px; } -.emoji-1F251 { background-position: -340px -260px; } -.emoji-1F300 { background-position: -340px -280px; } -.emoji-1F301 { background-position: -340px -300px; } -.emoji-1F302 { background-position: -340px -320px; } -.emoji-1F303 { background-position: 0 -340px; } -.emoji-1F304 { background-position: -20px -340px; } -.emoji-1F305 { background-position: -40px -340px; } -.emoji-1F306 { background-position: -60px -340px; } -.emoji-1F307 { background-position: -80px -340px; } -.emoji-1F308 { background-position: -100px -340px; } -.emoji-1F309 { background-position: -120px -340px; } -.emoji-1F30A { background-position: -140px -340px; } -.emoji-1F30B { background-position: -160px -340px; } -.emoji-1F30C { background-position: -180px -340px; } -.emoji-1F30D { background-position: -200px -340px; } -.emoji-1F30E { background-position: -220px -340px; } -.emoji-1F30F { background-position: -240px -340px; } -.emoji-1F310 { background-position: -260px -340px; } -.emoji-1F311 { background-position: -280px -340px; } -.emoji-1F312 { background-position: -300px -340px; } -.emoji-1F313 { background-position: -320px -340px; } -.emoji-1F314 { background-position: -340px -340px; } -.emoji-1F315 { background-position: -360px 0; } -.emoji-1F316 { background-position: -360px -20px; } -.emoji-1F317 { background-position: -360px -40px; } -.emoji-1F318 { background-position: -360px -60px; } -.emoji-1F319 { background-position: -360px -80px; } -.emoji-1F31A { background-position: -360px -100px; } -.emoji-1F31B { background-position: -360px -120px; } -.emoji-1F31C { background-position: -360px -140px; } -.emoji-1F31D { background-position: -360px -160px; } -.emoji-1F31E { background-position: -360px -180px; } -.emoji-1F31F { background-position: -360px -200px; } -.emoji-1F320 { background-position: -360px -220px; } -.emoji-1F321 { background-position: -360px -240px; } -.emoji-1F324 { background-position: -360px -260px; } -.emoji-1F325 { background-position: -360px -280px; } -.emoji-1F326 { background-position: -360px -300px; } -.emoji-1F327 { background-position: -360px -320px; } -.emoji-1F328 { background-position: -360px -340px; } -.emoji-1F329 { background-position: 0 -360px; } -.emoji-1F32A { background-position: -20px -360px; } -.emoji-1F32B { background-position: -40px -360px; } -.emoji-1F32C { background-position: -60px -360px; } -.emoji-1F32D { background-position: -80px -360px; } -.emoji-1F32E { background-position: -100px -360px; } -.emoji-1F32F { background-position: -120px -360px; } -.emoji-1F330 { background-position: -140px -360px; } -.emoji-1F331 { background-position: -160px -360px; } -.emoji-1F332 { background-position: -180px -360px; } -.emoji-1F333 { background-position: -200px -360px; } -.emoji-1F334 { background-position: -220px -360px; } -.emoji-1F335 { background-position: -240px -360px; } -.emoji-1F336 { background-position: -260px -360px; } -.emoji-1F337 { background-position: -280px -360px; } -.emoji-1F338 { background-position: -300px -360px; } -.emoji-1F339 { background-position: -320px -360px; } -.emoji-1F33A { background-position: -340px -360px; } -.emoji-1F33B { background-position: -360px -360px; } -.emoji-1F33C { background-position: -380px 0; } -.emoji-1F33D { background-position: -380px -20px; } -.emoji-1F33E { background-position: -380px -40px; } -.emoji-1F33F { background-position: -380px -60px; } -.emoji-1F340 { background-position: -380px -80px; } -.emoji-1F341 { background-position: -380px -100px; } -.emoji-1F342 { background-position: -380px -120px; } -.emoji-1F343 { background-position: -380px -140px; } -.emoji-1F344 { background-position: -380px -160px; } -.emoji-1F345 { background-position: -380px -180px; } -.emoji-1F346 { background-position: -380px -200px; } -.emoji-1F347 { background-position: -380px -220px; } -.emoji-1F348 { background-position: -380px -240px; } -.emoji-1F349 { background-position: -380px -260px; } -.emoji-1F34A { background-position: -380px -280px; } -.emoji-1F34B { background-position: -380px -300px; } -.emoji-1F34C { background-position: -380px -320px; } -.emoji-1F34D { background-position: -380px -340px; } -.emoji-1F34E { background-position: -380px -360px; } -.emoji-1F34F { background-position: 0 -380px; } -.emoji-1F350 { background-position: -20px -380px; } -.emoji-1F351 { background-position: -40px -380px; } -.emoji-1F352 { background-position: -60px -380px; } -.emoji-1F353 { background-position: -80px -380px; } -.emoji-1F354 { background-position: -100px -380px; } -.emoji-1F355 { background-position: -120px -380px; } -.emoji-1F356 { background-position: -140px -380px; } -.emoji-1F357 { background-position: -160px -380px; } -.emoji-1F358 { background-position: -180px -380px; } -.emoji-1F359 { background-position: -200px -380px; } -.emoji-1F35A { background-position: -220px -380px; } -.emoji-1F35B { background-position: -240px -380px; } -.emoji-1F35C { background-position: -260px -380px; } -.emoji-1F35D { background-position: -280px -380px; } -.emoji-1F35E { background-position: -300px -380px; } -.emoji-1F35F { background-position: -320px -380px; } -.emoji-1F360 { background-position: -340px -380px; } -.emoji-1F361 { background-position: -360px -380px; } -.emoji-1F362 { background-position: -380px -380px; } -.emoji-1F363 { background-position: -400px 0; } -.emoji-1F364 { background-position: -400px -20px; } -.emoji-1F365 { background-position: -400px -40px; } -.emoji-1F366 { background-position: -400px -60px; } -.emoji-1F367 { background-position: -400px -80px; } -.emoji-1F368 { background-position: -400px -100px; } -.emoji-1F369 { background-position: -400px -120px; } -.emoji-1F36A { background-position: -400px -140px; } -.emoji-1F36B { background-position: -400px -160px; } -.emoji-1F36C { background-position: -400px -180px; } -.emoji-1F36D { background-position: -400px -200px; } -.emoji-1F36E { background-position: -400px -220px; } -.emoji-1F36F { background-position: -400px -240px; } -.emoji-1F370 { background-position: -400px -260px; } -.emoji-1F371 { background-position: -400px -280px; } -.emoji-1F372 { background-position: -400px -300px; } -.emoji-1F373 { background-position: -400px -320px; } -.emoji-1F374 { background-position: -400px -340px; } -.emoji-1F375 { background-position: -400px -360px; } -.emoji-1F376 { background-position: -400px -380px; } -.emoji-1F377 { background-position: 0 -400px; } -.emoji-1F378 { background-position: -20px -400px; } -.emoji-1F379 { background-position: -40px -400px; } -.emoji-1F37A { background-position: -60px -400px; } -.emoji-1F37B { background-position: -80px -400px; } -.emoji-1F37C { background-position: -100px -400px; } -.emoji-1F37D { background-position: -120px -400px; } -.emoji-1F37E { background-position: -140px -400px; } -.emoji-1F37F { background-position: -160px -400px; } -.emoji-1F380 { background-position: -180px -400px; } -.emoji-1F381 { background-position: -200px -400px; } -.emoji-1F382 { background-position: -220px -400px; } -.emoji-1F383 { background-position: -240px -400px; } -.emoji-1F384 { background-position: -260px -400px; } -.emoji-1F385 { background-position: -280px -400px; } -.emoji-1F385-1F3FB { background-position: -300px -400px; } -.emoji-1F385-1F3FC { background-position: -320px -400px; } -.emoji-1F385-1F3FD { background-position: -340px -400px; } -.emoji-1F385-1F3FE { background-position: -360px -400px; } -.emoji-1F385-1F3FF { background-position: -380px -400px; } -.emoji-1F386 { background-position: -400px -400px; } -.emoji-1F387 { background-position: -420px 0; } -.emoji-1F388 { background-position: -420px -20px; } -.emoji-1F389 { background-position: -420px -40px; } -.emoji-1F38A { background-position: -420px -60px; } -.emoji-1F38B { background-position: -420px -80px; } -.emoji-1F38C { background-position: -420px -100px; } -.emoji-1F38D { background-position: -420px -120px; } -.emoji-1F38E { background-position: -420px -140px; } -.emoji-1F38F { background-position: -420px -160px; } -.emoji-1F390 { background-position: -420px -180px; } -.emoji-1F391 { background-position: -420px -200px; } -.emoji-1F392 { background-position: -420px -220px; } -.emoji-1F393 { background-position: -420px -240px; } -.emoji-1F396 { background-position: -420px -260px; } -.emoji-1F397 { background-position: -420px -280px; } -.emoji-1F399 { background-position: -420px -300px; } -.emoji-1F39A { background-position: -420px -320px; } -.emoji-1F39B { background-position: -420px -340px; } -.emoji-1F39E { background-position: -420px -360px; } -.emoji-1F39F { background-position: -420px -380px; } -.emoji-1F3A0 { background-position: -420px -400px; } -.emoji-1F3A1 { background-position: 0 -420px; } -.emoji-1F3A2 { background-position: -20px -420px; } -.emoji-1F3A3 { background-position: -40px -420px; } -.emoji-1F3A4 { background-position: -60px -420px; } -.emoji-1F3A5 { background-position: -80px -420px; } -.emoji-1F3A6 { background-position: -100px -420px; } -.emoji-1F3A7 { background-position: -120px -420px; } -.emoji-1F3A8 { background-position: -140px -420px; } -.emoji-1F3A9 { background-position: -160px -420px; } -.emoji-1F3AA { background-position: -180px -420px; } -.emoji-1F3AB { background-position: -200px -420px; } -.emoji-1F3AC { background-position: -220px -420px; } -.emoji-1F3AD { background-position: -240px -420px; } -.emoji-1F3AE { background-position: -260px -420px; } -.emoji-1F3AF { background-position: -280px -420px; } -.emoji-1F3B0 { background-position: -300px -420px; } -.emoji-1F3B1 { background-position: -320px -420px; } -.emoji-1F3B2 { background-position: -340px -420px; } -.emoji-1F3B3 { background-position: -360px -420px; } -.emoji-1F3B4 { background-position: -380px -420px; } -.emoji-1F3B5 { background-position: -400px -420px; } -.emoji-1F3B6 { background-position: -420px -420px; } -.emoji-1F3B7 { background-position: -440px 0; } -.emoji-1F3B8 { background-position: -440px -20px; } -.emoji-1F3B9 { background-position: -440px -40px; } -.emoji-1F3BA { background-position: -440px -60px; } -.emoji-1F3BB { background-position: -440px -80px; } -.emoji-1F3BC { background-position: -440px -100px; } -.emoji-1F3BD { background-position: -440px -120px; } -.emoji-1F3BE { background-position: -440px -140px; } -.emoji-1F3BF { background-position: -440px -160px; } -.emoji-1F3C0 { background-position: -440px -180px; } -.emoji-1F3C1 { background-position: -440px -200px; } -.emoji-1F3C2 { background-position: -440px -220px; } -.emoji-1F3C3 { background-position: -440px -240px; } -.emoji-1F3C3-1F3FB { background-position: -440px -260px; } -.emoji-1F3C3-1F3FC { background-position: -440px -280px; } -.emoji-1F3C3-1F3FD { background-position: -440px -300px; } -.emoji-1F3C3-1F3FE { background-position: -440px -320px; } -.emoji-1F3C3-1F3FF { background-position: -440px -340px; } -.emoji-1F3C4 { background-position: -440px -360px; } -.emoji-1F3C4-1F3FB { background-position: -440px -380px; } -.emoji-1F3C4-1F3FC { background-position: -440px -400px; } -.emoji-1F3C4-1F3FD { background-position: -440px -420px; } -.emoji-1F3C4-1F3FE { background-position: 0 -440px; } -.emoji-1F3C4-1F3FF { background-position: -20px -440px; } -.emoji-1F3C5 { background-position: -40px -440px; } -.emoji-1F3C6 { background-position: -60px -440px; } -.emoji-1F3C7 { background-position: -80px -440px; } -.emoji-1F3C7-1F3FB { background-position: -100px -440px; } -.emoji-1F3C7-1F3FC { background-position: -120px -440px; } -.emoji-1F3C7-1F3FD { background-position: -140px -440px; } -.emoji-1F3C7-1F3FE { background-position: -160px -440px; } -.emoji-1F3C7-1F3FF { background-position: -180px -440px; } -.emoji-1F3C8 { background-position: -200px -440px; } -.emoji-1F3C9 { background-position: -220px -440px; } -.emoji-1F3CA { background-position: -240px -440px; } -.emoji-1F3CA-1F3FB { background-position: -260px -440px; } -.emoji-1F3CA-1F3FC { background-position: -280px -440px; } -.emoji-1F3CA-1F3FD { background-position: -300px -440px; } -.emoji-1F3CA-1F3FE { background-position: -320px -440px; } -.emoji-1F3CA-1F3FF { background-position: -340px -440px; } -.emoji-1F3CB { background-position: -360px -440px; } -.emoji-1F3CB-1F3FB { background-position: -380px -440px; } -.emoji-1F3CB-1F3FC { background-position: -400px -440px; } -.emoji-1F3CB-1F3FD { background-position: -420px -440px; } -.emoji-1F3CB-1F3FE { background-position: -440px -440px; } -.emoji-1F3CB-1F3FF { background-position: -460px 0; } -.emoji-1F3CC { background-position: -460px -20px; } -.emoji-1F3CD { background-position: -460px -40px; } -.emoji-1F3CE { background-position: -460px -60px; } -.emoji-1F3CF { background-position: -460px -80px; } -.emoji-1F3D0 { background-position: -460px -100px; } -.emoji-1F3D1 { background-position: -460px -120px; } -.emoji-1F3D2 { background-position: -460px -140px; } -.emoji-1F3D3 { background-position: -460px -160px; } -.emoji-1F3D4 { background-position: -460px -180px; } -.emoji-1F3D5 { background-position: -460px -200px; } -.emoji-1F3D6 { background-position: -460px -220px; } -.emoji-1F3D7 { background-position: -460px -240px; } -.emoji-1F3D8 { background-position: -460px -260px; } -.emoji-1F3D9 { background-position: -460px -280px; } -.emoji-1F3DA { background-position: -460px -300px; } -.emoji-1F3DB { background-position: -460px -320px; } -.emoji-1F3DC { background-position: -460px -340px; } -.emoji-1F3DD { background-position: -460px -360px; } -.emoji-1F3DE { background-position: -460px -380px; } -.emoji-1F3DF { background-position: -460px -400px; } -.emoji-1F3E0 { background-position: -460px -420px; } -.emoji-1F3E1 { background-position: -460px -440px; } -.emoji-1F3E2 { background-position: 0 -460px; } -.emoji-1F3E3 { background-position: -20px -460px; } -.emoji-1F3E4 { background-position: -40px -460px; } -.emoji-1F3E5 { background-position: -60px -460px; } -.emoji-1F3E6 { background-position: -80px -460px; } -.emoji-1F3E7 { background-position: -100px -460px; } -.emoji-1F3E8 { background-position: -120px -460px; } -.emoji-1F3E9 { background-position: -140px -460px; } -.emoji-1F3EA { background-position: -160px -460px; } -.emoji-1F3EB { background-position: -180px -460px; } -.emoji-1F3EC { background-position: -200px -460px; } -.emoji-1F3ED { background-position: -220px -460px; } -.emoji-1F3EE { background-position: -240px -460px; } -.emoji-1F3EF { background-position: -260px -460px; } -.emoji-1F3F0 { background-position: -280px -460px; } -.emoji-1F3F3 { background-position: -300px -460px; } -.emoji-1F3F4 { background-position: -320px -460px; } -.emoji-1F3F5 { background-position: -340px -460px; } -.emoji-1F3F7 { background-position: -360px -460px; } -.emoji-1F3F8 { background-position: -380px -460px; } -.emoji-1F3F9 { background-position: -400px -460px; } -.emoji-1F3FA { background-position: -420px -460px; } -.emoji-1F3FB { background-position: -440px -460px; } -.emoji-1F3FC { background-position: -460px -460px; } -.emoji-1F3FD { background-position: -480px 0; } -.emoji-1F3FE { background-position: -480px -20px; } -.emoji-1F3FF { background-position: -480px -40px; } -.emoji-1F400 { background-position: -480px -60px; } -.emoji-1F401 { background-position: -480px -80px; } -.emoji-1F402 { background-position: -480px -100px; } -.emoji-1F403 { background-position: -480px -120px; } -.emoji-1F404 { background-position: -480px -140px; } -.emoji-1F405 { background-position: -480px -160px; } -.emoji-1F406 { background-position: -480px -180px; } -.emoji-1F407 { background-position: -480px -200px; } -.emoji-1F408 { background-position: -480px -220px; } -.emoji-1F409 { background-position: -480px -240px; } -.emoji-1F40A { background-position: -480px -260px; } -.emoji-1F40B { background-position: -480px -280px; } -.emoji-1F40C { background-position: -480px -300px; } -.emoji-1F40D { background-position: -480px -320px; } -.emoji-1F40E { background-position: -480px -340px; } -.emoji-1F40F { background-position: -480px -360px; } -.emoji-1F410 { background-position: -480px -380px; } -.emoji-1F411 { background-position: -480px -400px; } -.emoji-1F412 { background-position: -480px -420px; } -.emoji-1F413 { background-position: -480px -440px; } -.emoji-1F414 { background-position: -480px -460px; } -.emoji-1F415 { background-position: 0 -480px; } -.emoji-1F416 { background-position: -20px -480px; } -.emoji-1F417 { background-position: -40px -480px; } -.emoji-1F418 { background-position: -60px -480px; } -.emoji-1F419 { background-position: -80px -480px; } -.emoji-1F41A { background-position: -100px -480px; } -.emoji-1F41B { background-position: -120px -480px; } -.emoji-1F41C { background-position: -140px -480px; } -.emoji-1F41D { background-position: -160px -480px; } -.emoji-1F41E { background-position: -180px -480px; } -.emoji-1F41F { background-position: -200px -480px; } -.emoji-1F420 { background-position: -220px -480px; } -.emoji-1F421 { background-position: -240px -480px; } -.emoji-1F422 { background-position: -260px -480px; } -.emoji-1F423 { background-position: -280px -480px; } -.emoji-1F424 { background-position: -300px -480px; } -.emoji-1F425 { background-position: -320px -480px; } -.emoji-1F426 { background-position: -340px -480px; } -.emoji-1F427 { background-position: -360px -480px; } -.emoji-1F428 { background-position: -380px -480px; } -.emoji-1F429 { background-position: -400px -480px; } -.emoji-1F42A { background-position: -420px -480px; } -.emoji-1F42B { background-position: -440px -480px; } -.emoji-1F42C { background-position: -460px -480px; } -.emoji-1F42D { background-position: -480px -480px; } -.emoji-1F42E { background-position: -500px 0; } -.emoji-1F42F { background-position: -500px -20px; } -.emoji-1F430 { background-position: -500px -40px; } -.emoji-1F431 { background-position: -500px -60px; } -.emoji-1F432 { background-position: -500px -80px; } -.emoji-1F433 { background-position: -500px -100px; } -.emoji-1F434 { background-position: -500px -120px; } -.emoji-1F435 { background-position: -500px -140px; } -.emoji-1F436 { background-position: -500px -160px; } -.emoji-1F437 { background-position: -500px -180px; } -.emoji-1F438 { background-position: -500px -200px; } -.emoji-1F439 { background-position: -500px -220px; } -.emoji-1F43A { background-position: -500px -240px; } -.emoji-1F43B { background-position: -500px -260px; } -.emoji-1F43C { background-position: -500px -280px; } -.emoji-1F43D { background-position: -500px -300px; } -.emoji-1F43E { background-position: -500px -320px; } -.emoji-1F43F { background-position: -500px -340px; } -.emoji-1F440 { background-position: -500px -360px; } -.emoji-1F441 { background-position: -500px -380px; } -.emoji-1F441-1F5E8 { background-position: -500px -400px; } -.emoji-1F442 { background-position: -500px -420px; } -.emoji-1F442-1F3FB { background-position: -500px -440px; } -.emoji-1F442-1F3FC { background-position: -500px -460px; } -.emoji-1F442-1F3FD { background-position: -500px -480px; } -.emoji-1F442-1F3FE { background-position: 0 -500px; } -.emoji-1F442-1F3FF { background-position: -20px -500px; } -.emoji-1F443 { background-position: -40px -500px; } -.emoji-1F443-1F3FB { background-position: -60px -500px; } -.emoji-1F443-1F3FC { background-position: -80px -500px; } -.emoji-1F443-1F3FD { background-position: -100px -500px; } -.emoji-1F443-1F3FE { background-position: -120px -500px; } -.emoji-1F443-1F3FF { background-position: -140px -500px; } -.emoji-1F444 { background-position: -160px -500px; } -.emoji-1F445 { background-position: -180px -500px; } -.emoji-1F446 { background-position: -200px -500px; } -.emoji-1F446-1F3FB { background-position: -220px -500px; } -.emoji-1F446-1F3FC { background-position: -240px -500px; } -.emoji-1F446-1F3FD { background-position: -260px -500px; } -.emoji-1F446-1F3FE { background-position: -280px -500px; } -.emoji-1F446-1F3FF { background-position: -300px -500px; } -.emoji-1F447 { background-position: -320px -500px; } -.emoji-1F447-1F3FB { background-position: -340px -500px; } -.emoji-1F447-1F3FC { background-position: -360px -500px; } -.emoji-1F447-1F3FD { background-position: -380px -500px; } -.emoji-1F447-1F3FE { background-position: -400px -500px; } -.emoji-1F447-1F3FF { background-position: -420px -500px; } -.emoji-1F448 { background-position: -440px -500px; } -.emoji-1F448-1F3FB { background-position: -460px -500px; } -.emoji-1F448-1F3FC { background-position: -480px -500px; } -.emoji-1F448-1F3FD { background-position: -500px -500px; } -.emoji-1F448-1F3FE { background-position: -520px 0; } -.emoji-1F448-1F3FF { background-position: -520px -20px; } -.emoji-1F449 { background-position: -520px -40px; } -.emoji-1F449-1F3FB { background-position: -520px -60px; } -.emoji-1F449-1F3FC { background-position: -520px -80px; } -.emoji-1F449-1F3FD { background-position: -520px -100px; } -.emoji-1F449-1F3FE { background-position: -520px -120px; } -.emoji-1F449-1F3FF { background-position: -520px -140px; } -.emoji-1F44A { background-position: -520px -160px; } -.emoji-1F44A-1F3FB { background-position: -520px -180px; } -.emoji-1F44A-1F3FC { background-position: -520px -200px; } -.emoji-1F44A-1F3FD { background-position: -520px -220px; } -.emoji-1F44A-1F3FE { background-position: -520px -240px; } -.emoji-1F44A-1F3FF { background-position: -520px -260px; } -.emoji-1F44B { background-position: -520px -280px; } -.emoji-1F44B-1F3FB { background-position: -520px -300px; } -.emoji-1F44B-1F3FC { background-position: -520px -320px; } -.emoji-1F44B-1F3FD { background-position: -520px -340px; } -.emoji-1F44B-1F3FE { background-position: -520px -360px; } -.emoji-1F44B-1F3FF { background-position: -520px -380px; } -.emoji-1F44C { background-position: -520px -400px; } -.emoji-1F44C-1F3FB { background-position: -520px -420px; } -.emoji-1F44C-1F3FC { background-position: -520px -440px; } -.emoji-1F44C-1F3FD { background-position: -520px -460px; } -.emoji-1F44C-1F3FE { background-position: -520px -480px; } -.emoji-1F44C-1F3FF { background-position: -520px -500px; } -.emoji-1F44D { background-position: 0 -520px; } -.emoji-1F44D-1F3FB { background-position: -20px -520px; } -.emoji-1F44D-1F3FC { background-position: -40px -520px; } -.emoji-1F44D-1F3FD { background-position: -60px -520px; } -.emoji-1F44D-1F3FE { background-position: -80px -520px; } -.emoji-1F44D-1F3FF { background-position: -100px -520px; } -.emoji-1F44E { background-position: -120px -520px; } -.emoji-1F44E-1F3FB { background-position: -140px -520px; } -.emoji-1F44E-1F3FC { background-position: -160px -520px; } -.emoji-1F44E-1F3FD { background-position: -180px -520px; } -.emoji-1F44E-1F3FE { background-position: -200px -520px; } -.emoji-1F44E-1F3FF { background-position: -220px -520px; } -.emoji-1F44F { background-position: -240px -520px; } -.emoji-1F44F-1F3FB { background-position: -260px -520px; } -.emoji-1F44F-1F3FC { background-position: -280px -520px; } -.emoji-1F44F-1F3FD { background-position: -300px -520px; } -.emoji-1F44F-1F3FE { background-position: -320px -520px; } -.emoji-1F44F-1F3FF { background-position: -340px -520px; } -.emoji-1F450 { background-position: -360px -520px; } -.emoji-1F450-1F3FB { background-position: -380px -520px; } -.emoji-1F450-1F3FC { background-position: -400px -520px; } -.emoji-1F450-1F3FD { background-position: -420px -520px; } -.emoji-1F450-1F3FE { background-position: -440px -520px; } -.emoji-1F450-1F3FF { background-position: -460px -520px; } -.emoji-1F451 { background-position: -480px -520px; } -.emoji-1F452 { background-position: -500px -520px; } -.emoji-1F453 { background-position: -520px -520px; } -.emoji-1F454 { background-position: -540px 0; } -.emoji-1F455 { background-position: -540px -20px; } -.emoji-1F456 { background-position: -540px -40px; } -.emoji-1F457 { background-position: -540px -60px; } -.emoji-1F458 { background-position: -540px -80px; } -.emoji-1F459 { background-position: -540px -100px; } -.emoji-1F45A { background-position: -540px -120px; } -.emoji-1F45B { background-position: -540px -140px; } -.emoji-1F45C { background-position: -540px -160px; } -.emoji-1F45D { background-position: -540px -180px; } -.emoji-1F45E { background-position: -540px -200px; } -.emoji-1F45F { background-position: -540px -220px; } -.emoji-1F460 { background-position: -540px -240px; } -.emoji-1F461 { background-position: -540px -260px; } -.emoji-1F462 { background-position: -540px -280px; } -.emoji-1F463 { background-position: -540px -300px; } -.emoji-1F464 { background-position: -540px -320px; } -.emoji-1F465 { background-position: -540px -340px; } -.emoji-1F466 { background-position: -540px -360px; } -.emoji-1F466-1F3FB { background-position: -540px -380px; } -.emoji-1F466-1F3FC { background-position: -540px -400px; } -.emoji-1F466-1F3FD { background-position: -540px -420px; } -.emoji-1F466-1F3FE { background-position: -540px -440px; } -.emoji-1F466-1F3FF { background-position: -540px -460px; } -.emoji-1F467 { background-position: -540px -480px; } -.emoji-1F467-1F3FB { background-position: -540px -500px; } -.emoji-1F467-1F3FC { background-position: -540px -520px; } -.emoji-1F467-1F3FD { background-position: 0 -540px; } -.emoji-1F467-1F3FE { background-position: -20px -540px; } -.emoji-1F467-1F3FF { background-position: -40px -540px; } -.emoji-1F468 { background-position: -60px -540px; } -.emoji-1F468-1F3FB { background-position: -80px -540px; } -.emoji-1F468-1F3FC { background-position: -100px -540px; } -.emoji-1F468-1F3FD { background-position: -120px -540px; } -.emoji-1F468-1F3FE { background-position: -140px -540px; } -.emoji-1F468-1F3FF { background-position: -160px -540px; } -.emoji-1F468-1F468-1F466 { background-position: -180px -540px; } -.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; } -.emoji-1F468-1F468-1F467 { background-position: -220px -540px; } -.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; } -.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; } -.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; } -.emoji-1F468-1F469-1F467 { background-position: -300px -540px; } -.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; } -.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; } -.emoji-1F468-2764-1F468 { background-position: -360px -540px; } -.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; } -.emoji-1F469 { background-position: -400px -540px; } -.emoji-1F469-1F3FB { background-position: -420px -540px; } -.emoji-1F469-1F3FC { background-position: -440px -540px; } -.emoji-1F469-1F3FD { background-position: -460px -540px; } -.emoji-1F469-1F3FE { background-position: -480px -540px; } -.emoji-1F469-1F3FF { background-position: -500px -540px; } -.emoji-1F469-1F469-1F466 { background-position: -520px -540px; } -.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; } -.emoji-1F469-1F469-1F467 { background-position: -560px 0; } -.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; } -.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; } -.emoji-1F469-2764-1F469 { background-position: -560px -60px; } -.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; } -.emoji-1F46A { background-position: -560px -100px; } -.emoji-1F46B { background-position: -560px -120px; } -.emoji-1F46C { background-position: -560px -140px; } -.emoji-1F46D { background-position: -560px -160px; } -.emoji-1F46E { background-position: -560px -180px; } -.emoji-1F46E-1F3FB { background-position: -560px -200px; } -.emoji-1F46E-1F3FC { background-position: -560px -220px; } -.emoji-1F46E-1F3FD { background-position: -560px -240px; } -.emoji-1F46E-1F3FE { background-position: -560px -260px; } -.emoji-1F46E-1F3FF { background-position: -560px -280px; } -.emoji-1F46F { background-position: -560px -300px; } -.emoji-1F470 { background-position: -560px -320px; } -.emoji-1F470-1F3FB { background-position: -560px -340px; } -.emoji-1F470-1F3FC { background-position: -560px -360px; } -.emoji-1F470-1F3FD { background-position: -560px -380px; } -.emoji-1F470-1F3FE { background-position: -560px -400px; } -.emoji-1F470-1F3FF { background-position: -560px -420px; } -.emoji-1F471 { background-position: -560px -440px; } -.emoji-1F471-1F3FB { background-position: -560px -460px; } -.emoji-1F471-1F3FC { background-position: -560px -480px; } -.emoji-1F471-1F3FD { background-position: -560px -500px; } -.emoji-1F471-1F3FE { background-position: -560px -520px; } -.emoji-1F471-1F3FF { background-position: -560px -540px; } -.emoji-1F472 { background-position: 0 -560px; } -.emoji-1F472-1F3FB { background-position: -20px -560px; } -.emoji-1F472-1F3FC { background-position: -40px -560px; } -.emoji-1F472-1F3FD { background-position: -60px -560px; } -.emoji-1F472-1F3FE { background-position: -80px -560px; } -.emoji-1F472-1F3FF { background-position: -100px -560px; } -.emoji-1F473 { background-position: -120px -560px; } -.emoji-1F473-1F3FB { background-position: -140px -560px; } -.emoji-1F473-1F3FC { background-position: -160px -560px; } -.emoji-1F473-1F3FD { background-position: -180px -560px; } -.emoji-1F473-1F3FE { background-position: -200px -560px; } -.emoji-1F473-1F3FF { background-position: -220px -560px; } -.emoji-1F474 { background-position: -240px -560px; } -.emoji-1F474-1F3FB { background-position: -260px -560px; } -.emoji-1F474-1F3FC { background-position: -280px -560px; } -.emoji-1F474-1F3FD { background-position: -300px -560px; } -.emoji-1F474-1F3FE { background-position: -320px -560px; } -.emoji-1F474-1F3FF { background-position: -340px -560px; } -.emoji-1F475 { background-position: -360px -560px; } -.emoji-1F475-1F3FB { background-position: -380px -560px; } -.emoji-1F475-1F3FC { background-position: -400px -560px; } -.emoji-1F475-1F3FD { background-position: -420px -560px; } -.emoji-1F475-1F3FE { background-position: -440px -560px; } -.emoji-1F475-1F3FF { background-position: -460px -560px; } -.emoji-1F476 { background-position: -480px -560px; } -.emoji-1F476-1F3FB { background-position: -500px -560px; } -.emoji-1F476-1F3FC { background-position: -520px -560px; } -.emoji-1F476-1F3FD { background-position: -540px -560px; } -.emoji-1F476-1F3FE { background-position: -560px -560px; } -.emoji-1F476-1F3FF { background-position: -580px 0; } -.emoji-1F477 { background-position: -580px -20px; } -.emoji-1F477-1F3FB { background-position: -580px -40px; } -.emoji-1F477-1F3FC { background-position: -580px -60px; } -.emoji-1F477-1F3FD { background-position: -580px -80px; } -.emoji-1F477-1F3FE { background-position: -580px -100px; } -.emoji-1F477-1F3FF { background-position: -580px -120px; } -.emoji-1F478 { background-position: -580px -140px; } -.emoji-1F478-1F3FB { background-position: -580px -160px; } -.emoji-1F478-1F3FC { background-position: -580px -180px; } -.emoji-1F478-1F3FD { background-position: -580px -200px; } -.emoji-1F478-1F3FE { background-position: -580px -220px; } -.emoji-1F478-1F3FF { background-position: -580px -240px; } -.emoji-1F479 { background-position: -580px -260px; } -.emoji-1F47A { background-position: -580px -280px; } -.emoji-1F47B { background-position: -580px -300px; } -.emoji-1F47C { background-position: -580px -320px; } -.emoji-1F47C-1F3FB { background-position: -580px -340px; } -.emoji-1F47C-1F3FC { background-position: -580px -360px; } -.emoji-1F47C-1F3FD { background-position: -580px -380px; } -.emoji-1F47C-1F3FE { background-position: -580px -400px; } -.emoji-1F47C-1F3FF { background-position: -580px -420px; } -.emoji-1F47D { background-position: -580px -440px; } -.emoji-1F47E { background-position: -580px -460px; } -.emoji-1F47F { background-position: -580px -480px; } -.emoji-1F480 { background-position: -580px -500px; } -.emoji-1F481 { background-position: -580px -520px; } -.emoji-1F481-1F3FB { background-position: -580px -540px; } -.emoji-1F481-1F3FC { background-position: -580px -560px; } -.emoji-1F481-1F3FD { background-position: 0 -580px; } -.emoji-1F481-1F3FE { background-position: -20px -580px; } -.emoji-1F481-1F3FF { background-position: -40px -580px; } -.emoji-1F482 { background-position: -60px -580px; } -.emoji-1F482-1F3FB { background-position: -80px -580px; } -.emoji-1F482-1F3FC { background-position: -100px -580px; } -.emoji-1F482-1F3FD { background-position: -120px -580px; } -.emoji-1F482-1F3FE { background-position: -140px -580px; } -.emoji-1F482-1F3FF { background-position: -160px -580px; } -.emoji-1F483 { background-position: -180px -580px; } -.emoji-1F483-1F3FB { background-position: -200px -580px; } -.emoji-1F483-1F3FC { background-position: -220px -580px; } -.emoji-1F483-1F3FD { background-position: -240px -580px; } -.emoji-1F483-1F3FE { background-position: -260px -580px; } -.emoji-1F483-1F3FF { background-position: -280px -580px; } -.emoji-1F484 { background-position: -300px -580px; } -.emoji-1F485 { background-position: -320px -580px; } -.emoji-1F485-1F3FB { background-position: -340px -580px; } -.emoji-1F485-1F3FC { background-position: -360px -580px; } -.emoji-1F485-1F3FD { background-position: -380px -580px; } -.emoji-1F485-1F3FE { background-position: -400px -580px; } -.emoji-1F485-1F3FF { background-position: -420px -580px; } -.emoji-1F486 { background-position: -440px -580px; } -.emoji-1F486-1F3FB { background-position: -460px -580px; } -.emoji-1F486-1F3FC { background-position: -480px -580px; } -.emoji-1F486-1F3FD { background-position: -500px -580px; } -.emoji-1F486-1F3FE { background-position: -520px -580px; } -.emoji-1F486-1F3FF { background-position: -540px -580px; } -.emoji-1F487 { background-position: -560px -580px; } -.emoji-1F487-1F3FB { background-position: -580px -580px; } -.emoji-1F487-1F3FC { background-position: -600px 0; } -.emoji-1F487-1F3FD { background-position: -600px -20px; } -.emoji-1F487-1F3FE { background-position: -600px -40px; } -.emoji-1F487-1F3FF { background-position: -600px -60px; } -.emoji-1F488 { background-position: -600px -80px; } -.emoji-1F489 { background-position: -600px -100px; } -.emoji-1F48A { background-position: -600px -120px; } -.emoji-1F48B { background-position: -600px -140px; } -.emoji-1F48C { background-position: -600px -160px; } -.emoji-1F48D { background-position: -600px -180px; } -.emoji-1F48E { background-position: -600px -200px; } -.emoji-1F48F { background-position: -600px -220px; } -.emoji-1F490 { background-position: -600px -240px; } -.emoji-1F491 { background-position: -600px -260px; } -.emoji-1F492 { background-position: -600px -280px; } -.emoji-1F493 { background-position: -600px -300px; } -.emoji-1F494 { background-position: -600px -320px; } -.emoji-1F495 { background-position: -600px -340px; } -.emoji-1F496 { background-position: -600px -360px; } -.emoji-1F497 { background-position: -600px -380px; } -.emoji-1F498 { background-position: -600px -400px; } -.emoji-1F499 { background-position: -600px -420px; } -.emoji-1F49A { background-position: -600px -440px; } -.emoji-1F49B { background-position: -600px -460px; } -.emoji-1F49C { background-position: -600px -480px; } -.emoji-1F49D { background-position: -600px -500px; } -.emoji-1F49E { background-position: -600px -520px; } -.emoji-1F49F { background-position: -600px -540px; } -.emoji-1F4A0 { background-position: -600px -560px; } -.emoji-1F4A1 { background-position: -600px -580px; } -.emoji-1F4A2 { background-position: 0 -600px; } -.emoji-1F4A3 { background-position: -20px -600px; } -.emoji-1F4A4 { background-position: -40px -600px; } -.emoji-1F4A5 { background-position: -60px -600px; } -.emoji-1F4A6 { background-position: -80px -600px; } -.emoji-1F4A7 { background-position: -100px -600px; } -.emoji-1F4A8 { background-position: -120px -600px; } -.emoji-1F4A9 { background-position: -140px -600px; } -.emoji-1F4AA { background-position: -160px -600px; } -.emoji-1F4AA-1F3FB { background-position: -180px -600px; } -.emoji-1F4AA-1F3FC { background-position: -200px -600px; } -.emoji-1F4AA-1F3FD { background-position: -220px -600px; } -.emoji-1F4AA-1F3FE { background-position: -240px -600px; } -.emoji-1F4AA-1F3FF { background-position: -260px -600px; } -.emoji-1F4AB { background-position: -280px -600px; } -.emoji-1F4AC { background-position: -300px -600px; } -.emoji-1F4AD { background-position: -320px -600px; } -.emoji-1F4AE { background-position: -340px -600px; } -.emoji-1F4AF { background-position: -360px -600px; } -.emoji-1F4B0 { background-position: -380px -600px; } -.emoji-1F4B1 { background-position: -400px -600px; } -.emoji-1F4B2 { background-position: -420px -600px; } -.emoji-1F4B3 { background-position: -440px -600px; } -.emoji-1F4B4 { background-position: -460px -600px; } -.emoji-1F4B5 { background-position: -480px -600px; } -.emoji-1F4B6 { background-position: -500px -600px; } -.emoji-1F4B7 { background-position: -520px -600px; } -.emoji-1F4B8 { background-position: -540px -600px; } -.emoji-1F4B9 { background-position: -560px -600px; } -.emoji-1F4BA { background-position: -580px -600px; } -.emoji-1F4BB { background-position: -600px -600px; } -.emoji-1F4BC { background-position: -620px 0; } -.emoji-1F4BD { background-position: -620px -20px; } -.emoji-1F4BE { background-position: -620px -40px; } -.emoji-1F4BF { background-position: -620px -60px; } -.emoji-1F4C0 { background-position: -620px -80px; } -.emoji-1F4C1 { background-position: -620px -100px; } -.emoji-1F4C2 { background-position: -620px -120px; } -.emoji-1F4C3 { background-position: -620px -140px; } -.emoji-1F4C4 { background-position: -620px -160px; } -.emoji-1F4C5 { background-position: -620px -180px; } -.emoji-1F4C6 { background-position: -620px -200px; } -.emoji-1F4C7 { background-position: -620px -220px; } -.emoji-1F4C8 { background-position: -620px -240px; } -.emoji-1F4C9 { background-position: -620px -260px; } -.emoji-1F4CA { background-position: -620px -280px; } -.emoji-1F4CB { background-position: -620px -300px; } -.emoji-1F4CC { background-position: -620px -320px; } -.emoji-1F4CD { background-position: -620px -340px; } -.emoji-1F4CE { background-position: -620px -360px; } -.emoji-1F4CF { background-position: -620px -380px; } -.emoji-1F4D0 { background-position: -620px -400px; } -.emoji-1F4D1 { background-position: -620px -420px; } -.emoji-1F4D2 { background-position: -620px -440px; } -.emoji-1F4D3 { background-position: -620px -460px; } -.emoji-1F4D4 { background-position: -620px -480px; } -.emoji-1F4D5 { background-position: -620px -500px; } -.emoji-1F4D6 { background-position: -620px -520px; } -.emoji-1F4D7 { background-position: -620px -540px; } -.emoji-1F4D8 { background-position: -620px -560px; } -.emoji-1F4D9 { background-position: -620px -580px; } -.emoji-1F4DA { background-position: -620px -600px; } -.emoji-1F4DB { background-position: 0 -620px; } -.emoji-1F4DC { background-position: -20px -620px; } -.emoji-1F4DD { background-position: -40px -620px; } -.emoji-1F4DE { background-position: -60px -620px; } -.emoji-1F4DF { background-position: -80px -620px; } -.emoji-1F4E0 { background-position: -100px -620px; } -.emoji-1F4E1 { background-position: -120px -620px; } -.emoji-1F4E2 { background-position: -140px -620px; } -.emoji-1F4E3 { background-position: -160px -620px; } -.emoji-1F4E4 { background-position: -180px -620px; } -.emoji-1F4E5 { background-position: -200px -620px; } -.emoji-1F4E6 { background-position: -220px -620px; } -.emoji-1F4E7 { background-position: -240px -620px; } -.emoji-1F4E8 { background-position: -260px -620px; } -.emoji-1F4E9 { background-position: -280px -620px; } -.emoji-1F4EA { background-position: -300px -620px; } -.emoji-1F4EB { background-position: -320px -620px; } -.emoji-1F4EC { background-position: -340px -620px; } -.emoji-1F4ED { background-position: -360px -620px; } -.emoji-1F4EE { background-position: -380px -620px; } -.emoji-1F4EF { background-position: -400px -620px; } -.emoji-1F4F0 { background-position: -420px -620px; } -.emoji-1F4F1 { background-position: -440px -620px; } -.emoji-1F4F2 { background-position: -460px -620px; } -.emoji-1F4F3 { background-position: -480px -620px; } -.emoji-1F4F4 { background-position: -500px -620px; } -.emoji-1F4F5 { background-position: -520px -620px; } -.emoji-1F4F6 { background-position: -540px -620px; } -.emoji-1F4F7 { background-position: -560px -620px; } -.emoji-1F4F8 { background-position: -580px -620px; } -.emoji-1F4F9 { background-position: -600px -620px; } -.emoji-1F4FA { background-position: -620px -620px; } -.emoji-1F4FB { background-position: -640px 0; } -.emoji-1F4FC { background-position: -640px -20px; } -.emoji-1F4FD { background-position: -640px -40px; } -.emoji-1F4FF { background-position: -640px -60px; } -.emoji-1F500 { background-position: -640px -80px; } -.emoji-1F501 { background-position: -640px -100px; } -.emoji-1F502 { background-position: -640px -120px; } -.emoji-1F503 { background-position: -640px -140px; } -.emoji-1F504 { background-position: -640px -160px; } -.emoji-1F505 { background-position: -640px -180px; } -.emoji-1F506 { background-position: -640px -200px; } -.emoji-1F507 { background-position: -640px -220px; } -.emoji-1F508 { background-position: -640px -240px; } -.emoji-1F509 { background-position: -640px -260px; } -.emoji-1F50A { background-position: -640px -280px; } -.emoji-1F50B { background-position: -640px -300px; } -.emoji-1F50C { background-position: -640px -320px; } -.emoji-1F50D { background-position: -640px -340px; } -.emoji-1F50E { background-position: -640px -360px; } -.emoji-1F50F { background-position: -640px -380px; } -.emoji-1F510 { background-position: -640px -400px; } -.emoji-1F511 { background-position: -640px -420px; } -.emoji-1F512 { background-position: -640px -440px; } -.emoji-1F513 { background-position: -640px -460px; } -.emoji-1F514 { background-position: -640px -480px; } -.emoji-1F515 { background-position: -640px -500px; } -.emoji-1F516 { background-position: -640px -520px; } -.emoji-1F517 { background-position: -640px -540px; } -.emoji-1F518 { background-position: -640px -560px; } -.emoji-1F519 { background-position: -640px -580px; } -.emoji-1F51A { background-position: -640px -600px; } -.emoji-1F51B { background-position: -640px -620px; } -.emoji-1F51C { background-position: 0 -640px; } -.emoji-1F51D { background-position: -20px -640px; } -.emoji-1F51E { background-position: -40px -640px; } -.emoji-1F51F { background-position: -60px -640px; } -.emoji-1F520 { background-position: -80px -640px; } -.emoji-1F521 { background-position: -100px -640px; } -.emoji-1F522 { background-position: -120px -640px; } -.emoji-1F523 { background-position: -140px -640px; } -.emoji-1F524 { background-position: -160px -640px; } -.emoji-1F525 { background-position: -180px -640px; } -.emoji-1F526 { background-position: -200px -640px; } -.emoji-1F527 { background-position: -220px -640px; } -.emoji-1F528 { background-position: -240px -640px; } -.emoji-1F529 { background-position: -260px -640px; } -.emoji-1F52A { background-position: -280px -640px; } -.emoji-1F52B { background-position: -300px -640px; } -.emoji-1F52C { background-position: -320px -640px; } -.emoji-1F52D { background-position: -340px -640px; } -.emoji-1F52E { background-position: -360px -640px; } -.emoji-1F52F { background-position: -380px -640px; } -.emoji-1F530 { background-position: -400px -640px; } -.emoji-1F531 { background-position: -420px -640px; } -.emoji-1F532 { background-position: -440px -640px; } -.emoji-1F533 { background-position: -460px -640px; } -.emoji-1F534 { background-position: -480px -640px; } -.emoji-1F535 { background-position: -500px -640px; } -.emoji-1F536 { background-position: -520px -640px; } -.emoji-1F537 { background-position: -540px -640px; } -.emoji-1F538 { background-position: -560px -640px; } -.emoji-1F539 { background-position: -580px -640px; } -.emoji-1F53A { background-position: -600px -640px; } -.emoji-1F53B { background-position: -620px -640px; } -.emoji-1F53C { background-position: -640px -640px; } -.emoji-1F53D { background-position: -660px 0; } -.emoji-1F549 { background-position: -660px -20px; } -.emoji-1F54A { background-position: -660px -40px; } -.emoji-1F54B { background-position: -660px -60px; } -.emoji-1F54C { background-position: -660px -80px; } -.emoji-1F54D { background-position: -660px -100px; } -.emoji-1F54E { background-position: -660px -120px; } -.emoji-1F550 { background-position: -660px -140px; } -.emoji-1F551 { background-position: -660px -160px; } -.emoji-1F552 { background-position: -660px -180px; } -.emoji-1F553 { background-position: -660px -200px; } -.emoji-1F554 { background-position: -660px -220px; } -.emoji-1F555 { background-position: -660px -240px; } -.emoji-1F556 { background-position: -660px -260px; } -.emoji-1F557 { background-position: -660px -280px; } -.emoji-1F558 { background-position: -660px -300px; } -.emoji-1F559 { background-position: -660px -320px; } -.emoji-1F55A { background-position: -660px -340px; } -.emoji-1F55B { background-position: -660px -360px; } -.emoji-1F55C { background-position: -660px -380px; } -.emoji-1F55D { background-position: -660px -400px; } -.emoji-1F55E { background-position: -660px -420px; } -.emoji-1F55F { background-position: -660px -440px; } -.emoji-1F560 { background-position: -660px -460px; } -.emoji-1F561 { background-position: -660px -480px; } -.emoji-1F562 { background-position: -660px -500px; } -.emoji-1F563 { background-position: -660px -520px; } -.emoji-1F564 { background-position: -660px -540px; } -.emoji-1F565 { background-position: -660px -560px; } -.emoji-1F566 { background-position: -660px -580px; } -.emoji-1F567 { background-position: -660px -600px; } -.emoji-1F56F { background-position: -660px -620px; } -.emoji-1F570 { background-position: -660px -640px; } -.emoji-1F573 { background-position: 0 -660px; } -.emoji-1F574 { background-position: -20px -660px; } -.emoji-1F575 { background-position: -40px -660px; } -.emoji-1F575-1F3FB { background-position: -60px -660px; } -.emoji-1F575-1F3FC { background-position: -80px -660px; } -.emoji-1F575-1F3FD { background-position: -100px -660px; } -.emoji-1F575-1F3FE { background-position: -120px -660px; } -.emoji-1F575-1F3FF { background-position: -140px -660px; } -.emoji-1F576 { background-position: -160px -660px; } -.emoji-1F577 { background-position: -180px -660px; } -.emoji-1F578 { background-position: -200px -660px; } -.emoji-1F579 { background-position: -220px -660px; } -.emoji-1F57A { background-position: -240px -660px; } -.emoji-1F57A-1F3FB { background-position: -260px -660px; } -.emoji-1F57A-1F3FC { background-position: -280px -660px; } -.emoji-1F57A-1F3FD { background-position: -300px -660px; } -.emoji-1F57A-1F3FE { background-position: -320px -660px; } -.emoji-1F57A-1F3FF { background-position: -340px -660px; } -.emoji-1F587 { background-position: -360px -660px; } -.emoji-1F58A { background-position: -380px -660px; } -.emoji-1F58B { background-position: -400px -660px; } -.emoji-1F58C { background-position: -420px -660px; } -.emoji-1F58D { background-position: -440px -660px; } -.emoji-1F590 { background-position: -460px -660px; } -.emoji-1F590-1F3FB { background-position: -480px -660px; } -.emoji-1F590-1F3FC { background-position: -500px -660px; } -.emoji-1F590-1F3FD { background-position: -520px -660px; } -.emoji-1F590-1F3FE { background-position: -540px -660px; } -.emoji-1F590-1F3FF { background-position: -560px -660px; } -.emoji-1F595 { background-position: -580px -660px; } -.emoji-1F595-1F3FB { background-position: -600px -660px; } -.emoji-1F595-1F3FC { background-position: -620px -660px; } -.emoji-1F595-1F3FD { background-position: -640px -660px; } -.emoji-1F595-1F3FE { background-position: -660px -660px; } -.emoji-1F595-1F3FF { background-position: -680px 0; } -.emoji-1F596 { background-position: -680px -20px; } -.emoji-1F596-1F3FB { background-position: -680px -40px; } -.emoji-1F596-1F3FC { background-position: -680px -60px; } -.emoji-1F596-1F3FD { background-position: -680px -80px; } -.emoji-1F596-1F3FE { background-position: -680px -100px; } -.emoji-1F596-1F3FF { background-position: -680px -120px; } -.emoji-1F5A4 { background-position: -680px -140px; } -.emoji-1F5A5 { background-position: -680px -160px; } -.emoji-1F5A8 { background-position: -680px -180px; } -.emoji-1F5B1 { background-position: -680px -200px; } -.emoji-1F5B2 { background-position: -680px -220px; } -.emoji-1F5BC { background-position: -680px -240px; } -.emoji-1F5C2 { background-position: -680px -260px; } -.emoji-1F5C3 { background-position: -680px -280px; } -.emoji-1F5C4 { background-position: -680px -300px; } -.emoji-1F5D1 { background-position: -680px -320px; } -.emoji-1F5D2 { background-position: -680px -340px; } -.emoji-1F5D3 { background-position: -680px -360px; } -.emoji-1F5DC { background-position: -680px -380px; } -.emoji-1F5DD { background-position: -680px -400px; } -.emoji-1F5DE { background-position: -680px -420px; } -.emoji-1F5E1 { background-position: -680px -440px; } -.emoji-1F5E3 { background-position: -680px -460px; } -.emoji-1F5EF { background-position: -680px -480px; } -.emoji-1F5F3 { background-position: -680px -500px; } -.emoji-1F5FA { background-position: -680px -520px; } -.emoji-1F5FB { background-position: -680px -540px; } -.emoji-1F5FC { background-position: -680px -560px; } -.emoji-1F5FD { background-position: -680px -580px; } -.emoji-1F5FE { background-position: -680px -600px; } -.emoji-1F5FF { background-position: -680px -620px; } -.emoji-1F600 { background-position: -680px -640px; } -.emoji-1F601 { background-position: -680px -660px; } -.emoji-1F602 { background-position: 0 -680px; } -.emoji-1F603 { background-position: -20px -680px; } -.emoji-1F604 { background-position: -40px -680px; } -.emoji-1F605 { background-position: -60px -680px; } -.emoji-1F606 { background-position: -80px -680px; } -.emoji-1F607 { background-position: -100px -680px; } -.emoji-1F608 { background-position: -120px -680px; } -.emoji-1F609 { background-position: -140px -680px; } -.emoji-1F60A { background-position: -160px -680px; } -.emoji-1F60B { background-position: -180px -680px; } -.emoji-1F60C { background-position: -200px -680px; } -.emoji-1F60D { background-position: -220px -680px; } -.emoji-1F60E { background-position: -240px -680px; } -.emoji-1F60F { background-position: -260px -680px; } -.emoji-1F610 { background-position: -280px -680px; } -.emoji-1F611 { background-position: -300px -680px; } -.emoji-1F612 { background-position: -320px -680px; } -.emoji-1F613 { background-position: -340px -680px; } -.emoji-1F614 { background-position: -360px -680px; } -.emoji-1F615 { background-position: -380px -680px; } -.emoji-1F616 { background-position: -400px -680px; } -.emoji-1F617 { background-position: -420px -680px; } -.emoji-1F618 { background-position: -440px -680px; } -.emoji-1F619 { background-position: -460px -680px; } -.emoji-1F61A { background-position: -480px -680px; } -.emoji-1F61B { background-position: -500px -680px; } -.emoji-1F61C { background-position: -520px -680px; } -.emoji-1F61D { background-position: -540px -680px; } -.emoji-1F61E { background-position: -560px -680px; } -.emoji-1F61F { background-position: -580px -680px; } -.emoji-1F620 { background-position: -600px -680px; } -.emoji-1F621 { background-position: -620px -680px; } -.emoji-1F622 { background-position: -640px -680px; } -.emoji-1F623 { background-position: -660px -680px; } -.emoji-1F624 { background-position: -680px -680px; } -.emoji-1F625 { background-position: -700px 0; } -.emoji-1F626 { background-position: -700px -20px; } -.emoji-1F627 { background-position: -700px -40px; } -.emoji-1F628 { background-position: -700px -60px; } -.emoji-1F629 { background-position: -700px -80px; } -.emoji-1F62A { background-position: -700px -100px; } -.emoji-1F62B { background-position: -700px -120px; } -.emoji-1F62C { background-position: -700px -140px; } -.emoji-1F62D { background-position: -700px -160px; } -.emoji-1F62E { background-position: -700px -180px; } -.emoji-1F62F { background-position: -700px -200px; } -.emoji-1F630 { background-position: -700px -220px; } -.emoji-1F631 { background-position: -700px -240px; } -.emoji-1F632 { background-position: -700px -260px; } -.emoji-1F633 { background-position: -700px -280px; } -.emoji-1F634 { background-position: -700px -300px; } -.emoji-1F635 { background-position: -700px -320px; } -.emoji-1F636 { background-position: -700px -340px; } -.emoji-1F637 { background-position: -700px -360px; } -.emoji-1F638 { background-position: -700px -380px; } -.emoji-1F639 { background-position: -700px -400px; } -.emoji-1F63A { background-position: -700px -420px; } -.emoji-1F63B { background-position: -700px -440px; } -.emoji-1F63C { background-position: -700px -460px; } -.emoji-1F63D { background-position: -700px -480px; } -.emoji-1F63E { background-position: -700px -500px; } -.emoji-1F63F { background-position: -700px -520px; } -.emoji-1F640 { background-position: -700px -540px; } -.emoji-1F641 { background-position: -700px -560px; } -.emoji-1F642 { background-position: -700px -580px; } -.emoji-1F643 { background-position: -700px -600px; } -.emoji-1F644 { background-position: -700px -620px; } -.emoji-1F645 { background-position: -700px -640px; } -.emoji-1F645-1F3FB { background-position: -700px -660px; } -.emoji-1F645-1F3FC { background-position: -700px -680px; } -.emoji-1F645-1F3FD { background-position: 0 -700px; } -.emoji-1F645-1F3FE { background-position: -20px -700px; } -.emoji-1F645-1F3FF { background-position: -40px -700px; } -.emoji-1F646 { background-position: -60px -700px; } -.emoji-1F646-1F3FB { background-position: -80px -700px; } -.emoji-1F646-1F3FC { background-position: -100px -700px; } -.emoji-1F646-1F3FD { background-position: -120px -700px; } -.emoji-1F646-1F3FE { background-position: -140px -700px; } -.emoji-1F646-1F3FF { background-position: -160px -700px; } -.emoji-1F647 { background-position: -180px -700px; } -.emoji-1F647-1F3FB { background-position: -200px -700px; } -.emoji-1F647-1F3FC { background-position: -220px -700px; } -.emoji-1F647-1F3FD { background-position: -240px -700px; } -.emoji-1F647-1F3FE { background-position: -260px -700px; } -.emoji-1F647-1F3FF { background-position: -280px -700px; } -.emoji-1F648 { background-position: -300px -700px; } -.emoji-1F649 { background-position: -320px -700px; } -.emoji-1F64A { background-position: -340px -700px; } -.emoji-1F64B { background-position: -360px -700px; } -.emoji-1F64B-1F3FB { background-position: -380px -700px; } -.emoji-1F64B-1F3FC { background-position: -400px -700px; } -.emoji-1F64B-1F3FD { background-position: -420px -700px; } -.emoji-1F64B-1F3FE { background-position: -440px -700px; } -.emoji-1F64B-1F3FF { background-position: -460px -700px; } -.emoji-1F64C { background-position: -480px -700px; } -.emoji-1F64C-1F3FB { background-position: -500px -700px; } -.emoji-1F64C-1F3FC { background-position: -520px -700px; } -.emoji-1F64C-1F3FD { background-position: -540px -700px; } -.emoji-1F64C-1F3FE { background-position: -560px -700px; } -.emoji-1F64C-1F3FF { background-position: -580px -700px; } -.emoji-1F64D { background-position: -600px -700px; } -.emoji-1F64D-1F3FB { background-position: -620px -700px; } -.emoji-1F64D-1F3FC { background-position: -640px -700px; } -.emoji-1F64D-1F3FD { background-position: -660px -700px; } -.emoji-1F64D-1F3FE { background-position: -680px -700px; } -.emoji-1F64D-1F3FF { background-position: -700px -700px; } -.emoji-1F64E { background-position: -720px 0; } -.emoji-1F64E-1F3FB { background-position: -720px -20px; } -.emoji-1F64E-1F3FC { background-position: -720px -40px; } -.emoji-1F64E-1F3FD { background-position: -720px -60px; } -.emoji-1F64E-1F3FE { background-position: -720px -80px; } -.emoji-1F64E-1F3FF { background-position: -720px -100px; } -.emoji-1F64F { background-position: -720px -120px; } -.emoji-1F64F-1F3FB { background-position: -720px -140px; } -.emoji-1F64F-1F3FC { background-position: -720px -160px; } -.emoji-1F64F-1F3FD { background-position: -720px -180px; } -.emoji-1F64F-1F3FE { background-position: -720px -200px; } -.emoji-1F64F-1F3FF { background-position: -720px -220px; } -.emoji-1F680 { background-position: -720px -240px; } -.emoji-1F681 { background-position: -720px -260px; } -.emoji-1F682 { background-position: -720px -280px; } -.emoji-1F683 { background-position: -720px -300px; } -.emoji-1F684 { background-position: -720px -320px; } -.emoji-1F685 { background-position: -720px -340px; } -.emoji-1F686 { background-position: -720px -360px; } -.emoji-1F687 { background-position: -720px -380px; } -.emoji-1F688 { background-position: -720px -400px; } -.emoji-1F689 { background-position: -720px -420px; } -.emoji-1F68A { background-position: -720px -440px; } -.emoji-1F68B { background-position: -720px -460px; } -.emoji-1F68C { background-position: -720px -480px; } -.emoji-1F68D { background-position: -720px -500px; } -.emoji-1F68E { background-position: -720px -520px; } -.emoji-1F68F { background-position: -720px -540px; } -.emoji-1F690 { background-position: -720px -560px; } -.emoji-1F691 { background-position: -720px -580px; } -.emoji-1F692 { background-position: -720px -600px; } -.emoji-1F693 { background-position: -720px -620px; } -.emoji-1F694 { background-position: -720px -640px; } -.emoji-1F695 { background-position: -720px -660px; } -.emoji-1F696 { background-position: -720px -680px; } -.emoji-1F697 { background-position: -720px -700px; } -.emoji-1F698 { background-position: 0 -720px; } -.emoji-1F699 { background-position: -20px -720px; } -.emoji-1F69A { background-position: -40px -720px; } -.emoji-1F69B { background-position: -60px -720px; } -.emoji-1F69C { background-position: -80px -720px; } -.emoji-1F69D { background-position: -100px -720px; } -.emoji-1F69E { background-position: -120px -720px; } -.emoji-1F69F { background-position: -140px -720px; } -.emoji-1F6A0 { background-position: -160px -720px; } -.emoji-1F6A1 { background-position: -180px -720px; } -.emoji-1F6A2 { background-position: -200px -720px; } -.emoji-1F6A3 { background-position: -220px -720px; } -.emoji-1F6A3-1F3FB { background-position: -240px -720px; } -.emoji-1F6A3-1F3FC { background-position: -260px -720px; } -.emoji-1F6A3-1F3FD { background-position: -280px -720px; } -.emoji-1F6A3-1F3FE { background-position: -300px -720px; } -.emoji-1F6A3-1F3FF { background-position: -320px -720px; } -.emoji-1F6A4 { background-position: -340px -720px; } -.emoji-1F6A5 { background-position: -360px -720px; } -.emoji-1F6A6 { background-position: -380px -720px; } -.emoji-1F6A7 { background-position: -400px -720px; } -.emoji-1F6A8 { background-position: -420px -720px; } -.emoji-1F6A9 { background-position: -440px -720px; } -.emoji-1F6AA { background-position: -460px -720px; } -.emoji-1F6AB { background-position: -480px -720px; } -.emoji-1F6AC { background-position: -500px -720px; } -.emoji-1F6AD { background-position: -520px -720px; } -.emoji-1F6AE { background-position: -540px -720px; } -.emoji-1F6AF { background-position: -560px -720px; } -.emoji-1F6B0 { background-position: -580px -720px; } -.emoji-1F6B1 { background-position: -600px -720px; } -.emoji-1F6B2 { background-position: -620px -720px; } -.emoji-1F6B3 { background-position: -640px -720px; } -.emoji-1F6B4 { background-position: -660px -720px; } -.emoji-1F6B4-1F3FB { background-position: -680px -720px; } -.emoji-1F6B4-1F3FC { background-position: -700px -720px; } -.emoji-1F6B4-1F3FD { background-position: -720px -720px; } -.emoji-1F6B4-1F3FE { background-position: -740px 0; } -.emoji-1F6B4-1F3FF { background-position: -740px -20px; } -.emoji-1F6B5 { background-position: -740px -40px; } -.emoji-1F6B5-1F3FB { background-position: -740px -60px; } -.emoji-1F6B5-1F3FC { background-position: -740px -80px; } -.emoji-1F6B5-1F3FD { background-position: -740px -100px; } -.emoji-1F6B5-1F3FE { background-position: -740px -120px; } -.emoji-1F6B5-1F3FF { background-position: -740px -140px; } -.emoji-1F6B6 { background-position: -740px -160px; } -.emoji-1F6B6-1F3FB { background-position: -740px -180px; } -.emoji-1F6B6-1F3FC { background-position: -740px -200px; } -.emoji-1F6B6-1F3FD { background-position: -740px -220px; } -.emoji-1F6B6-1F3FE { background-position: -740px -240px; } -.emoji-1F6B6-1F3FF { background-position: -740px -260px; } -.emoji-1F6B7 { background-position: -740px -280px; } -.emoji-1F6B8 { background-position: -740px -300px; } -.emoji-1F6B9 { background-position: -740px -320px; } -.emoji-1F6BA { background-position: -740px -340px; } -.emoji-1F6BB { background-position: -740px -360px; } -.emoji-1F6BC { background-position: -740px -380px; } -.emoji-1F6BD { background-position: -740px -400px; } -.emoji-1F6BE { background-position: -740px -420px; } -.emoji-1F6BF { background-position: -740px -440px; } -.emoji-1F6C0 { background-position: -740px -460px; } -.emoji-1F6C0-1F3FB { background-position: -740px -480px; } -.emoji-1F6C0-1F3FC { background-position: -740px -500px; } -.emoji-1F6C0-1F3FD { background-position: -740px -520px; } -.emoji-1F6C0-1F3FE { background-position: -740px -540px; } -.emoji-1F6C0-1F3FF { background-position: -740px -560px; } -.emoji-1F6C1 { background-position: -740px -580px; } -.emoji-1F6C2 { background-position: -740px -600px; } -.emoji-1F6C3 { background-position: -740px -620px; } -.emoji-1F6C4 { background-position: -740px -640px; } -.emoji-1F6C5 { background-position: -740px -660px; } -.emoji-1F6CB { background-position: -740px -680px; } -.emoji-1F6CC { background-position: -740px -700px; } -.emoji-1F6CD { background-position: -740px -720px; } -.emoji-1F6CE { background-position: 0 -740px; } -.emoji-1F6CF { background-position: -20px -740px; } -.emoji-1F6D0 { background-position: -40px -740px; } -.emoji-1F6D1 { background-position: -60px -740px; } -.emoji-1F6D2 { background-position: -80px -740px; } -.emoji-1F6E0 { background-position: -100px -740px; } -.emoji-1F6E1 { background-position: -120px -740px; } -.emoji-1F6E2 { background-position: -140px -740px; } -.emoji-1F6E3 { background-position: -160px -740px; } -.emoji-1F6E4 { background-position: -180px -740px; } -.emoji-1F6E5 { background-position: -200px -740px; } -.emoji-1F6E9 { background-position: -220px -740px; } -.emoji-1F6EB { background-position: -240px -740px; } -.emoji-1F6EC { background-position: -260px -740px; } -.emoji-1F6F0 { background-position: -280px -740px; } -.emoji-1F6F3 { background-position: -300px -740px; } -.emoji-1F6F4 { background-position: -320px -740px; } -.emoji-1F6F5 { background-position: -340px -740px; } -.emoji-1F6F6 { background-position: -360px -740px; } -.emoji-1F910 { background-position: -380px -740px; } -.emoji-1F911 { background-position: -400px -740px; } -.emoji-1F912 { background-position: -420px -740px; } -.emoji-1F913 { background-position: -440px -740px; } -.emoji-1F914 { background-position: -460px -740px; } -.emoji-1F915 { background-position: -480px -740px; } -.emoji-1F916 { background-position: -500px -740px; } -.emoji-1F917 { background-position: -520px -740px; } -.emoji-1F918 { background-position: -540px -740px; } -.emoji-1F918-1F3FB { background-position: -560px -740px; } -.emoji-1F918-1F3FC { background-position: -580px -740px; } -.emoji-1F918-1F3FD { background-position: -600px -740px; } -.emoji-1F918-1F3FE { background-position: -620px -740px; } -.emoji-1F918-1F3FF { background-position: -640px -740px; } -.emoji-1F919 { background-position: -660px -740px; } -.emoji-1F919-1F3FB { background-position: -680px -740px; } -.emoji-1F919-1F3FC { background-position: -700px -740px; } -.emoji-1F919-1F3FD { background-position: -720px -740px; } -.emoji-1F919-1F3FE { background-position: -740px -740px; } -.emoji-1F919-1F3FF { background-position: -760px 0; } -.emoji-1F91A { background-position: -760px -20px; } -.emoji-1F91A-1F3FB { background-position: -760px -40px; } -.emoji-1F91A-1F3FC { background-position: -760px -60px; } -.emoji-1F91A-1F3FD { background-position: -760px -80px; } -.emoji-1F91A-1F3FE { background-position: -760px -100px; } -.emoji-1F91A-1F3FF { background-position: -760px -120px; } -.emoji-1F91B { background-position: -760px -140px; } -.emoji-1F91B-1F3FB { background-position: -760px -160px; } -.emoji-1F91B-1F3FC { background-position: -760px -180px; } -.emoji-1F91B-1F3FD { background-position: -760px -200px; } -.emoji-1F91B-1F3FE { background-position: -760px -220px; } -.emoji-1F91B-1F3FF { background-position: -760px -240px; } -.emoji-1F91C { background-position: -760px -260px; } -.emoji-1F91C-1F3FB { background-position: -760px -280px; } -.emoji-1F91C-1F3FC { background-position: -760px -300px; } -.emoji-1F91C-1F3FD { background-position: -760px -320px; } -.emoji-1F91C-1F3FE { background-position: -760px -340px; } -.emoji-1F91C-1F3FF { background-position: -760px -360px; } -.emoji-1F91D { background-position: -760px -380px; } -.emoji-1F91D-1F3FB { background-position: -760px -400px; } -.emoji-1F91D-1F3FC { background-position: -760px -420px; } -.emoji-1F91D-1F3FD { background-position: -760px -440px; } -.emoji-1F91D-1F3FE { background-position: -760px -460px; } -.emoji-1F91D-1F3FF { background-position: -760px -480px; } -.emoji-1F91E { background-position: -760px -500px; } -.emoji-1F91E-1F3FB { background-position: -760px -520px; } -.emoji-1F91E-1F3FC { background-position: -760px -540px; } -.emoji-1F91E-1F3FD { background-position: -760px -560px; } -.emoji-1F91E-1F3FE { background-position: -760px -580px; } -.emoji-1F91E-1F3FF { background-position: -760px -600px; } -.emoji-1F920 { background-position: -760px -620px; } -.emoji-1F921 { background-position: -760px -640px; } -.emoji-1F922 { background-position: -760px -660px; } -.emoji-1F923 { background-position: -760px -680px; } -.emoji-1F924 { background-position: -760px -700px; } -.emoji-1F925 { background-position: -760px -720px; } -.emoji-1F926 { background-position: -760px -740px; } -.emoji-1F926-1F3FB { background-position: 0 -760px; } -.emoji-1F926-1F3FC { background-position: -20px -760px; } -.emoji-1F926-1F3FD { background-position: -40px -760px; } -.emoji-1F926-1F3FE { background-position: -60px -760px; } -.emoji-1F926-1F3FF { background-position: -80px -760px; } -.emoji-1F927 { background-position: -100px -760px; } -.emoji-1F930 { background-position: -120px -760px; } -.emoji-1F930-1F3FB { background-position: -140px -760px; } -.emoji-1F930-1F3FC { background-position: -160px -760px; } -.emoji-1F930-1F3FD { background-position: -180px -760px; } -.emoji-1F930-1F3FE { background-position: -200px -760px; } -.emoji-1F930-1F3FF { background-position: -220px -760px; } -.emoji-1F933 { background-position: -240px -760px; } -.emoji-1F933-1F3FB { background-position: -260px -760px; } -.emoji-1F933-1F3FC { background-position: -280px -760px; } -.emoji-1F933-1F3FD { background-position: -300px -760px; } -.emoji-1F933-1F3FE { background-position: -320px -760px; } -.emoji-1F933-1F3FF { background-position: -340px -760px; } -.emoji-1F934 { background-position: -360px -760px; } -.emoji-1F934-1F3FB { background-position: -380px -760px; } -.emoji-1F934-1F3FC { background-position: -400px -760px; } -.emoji-1F934-1F3FD { background-position: -420px -760px; } -.emoji-1F934-1F3FE { background-position: -440px -760px; } -.emoji-1F934-1F3FF { background-position: -460px -760px; } -.emoji-1F935 { background-position: -480px -760px; } -.emoji-1F935-1F3FB { background-position: -500px -760px; } -.emoji-1F935-1F3FC { background-position: -520px -760px; } -.emoji-1F935-1F3FD { background-position: -540px -760px; } -.emoji-1F935-1F3FE { background-position: -560px -760px; } -.emoji-1F935-1F3FF { background-position: -580px -760px; } -.emoji-1F936 { background-position: -600px -760px; } -.emoji-1F936-1F3FB { background-position: -620px -760px; } -.emoji-1F936-1F3FC { background-position: -640px -760px; } -.emoji-1F936-1F3FD { background-position: -660px -760px; } -.emoji-1F936-1F3FE { background-position: -680px -760px; } -.emoji-1F936-1F3FF { background-position: -700px -760px; } -.emoji-1F937 { background-position: -720px -760px; } -.emoji-1F937-1F3FB { background-position: -740px -760px; } -.emoji-1F937-1F3FC { background-position: -760px -760px; } -.emoji-1F937-1F3FD { background-position: -780px 0; } -.emoji-1F937-1F3FE { background-position: -780px -20px; } -.emoji-1F937-1F3FF { background-position: -780px -40px; } -.emoji-1F938 { background-position: -780px -60px; } -.emoji-1F938-1F3FB { background-position: -780px -80px; } -.emoji-1F938-1F3FC { background-position: -780px -100px; } -.emoji-1F938-1F3FD { background-position: -780px -120px; } -.emoji-1F938-1F3FE { background-position: -780px -140px; } -.emoji-1F938-1F3FF { background-position: -780px -160px; } -.emoji-1F939 { background-position: -780px -180px; } -.emoji-1F939-1F3FB { background-position: -780px -200px; } -.emoji-1F939-1F3FC { background-position: -780px -220px; } -.emoji-1F939-1F3FD { background-position: -780px -240px; } -.emoji-1F939-1F3FE { background-position: -780px -260px; } -.emoji-1F939-1F3FF { background-position: -780px -280px; } -.emoji-1F93A { background-position: -780px -300px; } -.emoji-1F93C { background-position: -780px -320px; } -.emoji-1F93C-1F3FB { background-position: -780px -340px; } -.emoji-1F93C-1F3FC { background-position: -780px -360px; } -.emoji-1F93C-1F3FD { background-position: -780px -380px; } -.emoji-1F93C-1F3FE { background-position: -780px -400px; } -.emoji-1F93C-1F3FF { background-position: -780px -420px; } -.emoji-1F93D { background-position: -780px -440px; } -.emoji-1F93D-1F3FB { background-position: -780px -460px; } -.emoji-1F93D-1F3FC { background-position: -780px -480px; } -.emoji-1F93D-1F3FD { background-position: -780px -500px; } -.emoji-1F93D-1F3FE { background-position: -780px -520px; } -.emoji-1F93D-1F3FF { background-position: -780px -540px; } -.emoji-1F93E { background-position: -780px -560px; } -.emoji-1F93E-1F3FB { background-position: -780px -580px; } -.emoji-1F93E-1F3FC { background-position: -780px -600px; } -.emoji-1F93E-1F3FD { background-position: -780px -620px; } -.emoji-1F93E-1F3FE { background-position: -780px -640px; } -.emoji-1F93E-1F3FF { background-position: -780px -660px; } -.emoji-1F940 { background-position: -780px -680px; } -.emoji-1F941 { background-position: -780px -700px; } -.emoji-1F942 { background-position: -780px -720px; } -.emoji-1F943 { background-position: -780px -740px; } -.emoji-1F944 { background-position: -780px -760px; } -.emoji-1F945 { background-position: 0 -780px; } -.emoji-1F947 { background-position: -20px -780px; } -.emoji-1F948 { background-position: -40px -780px; } -.emoji-1F949 { background-position: -60px -780px; } -.emoji-1F94A { background-position: -80px -780px; } -.emoji-1F94B { background-position: -100px -780px; } -.emoji-1F950 { background-position: -120px -780px; } -.emoji-1F951 { background-position: -140px -780px; } -.emoji-1F952 { background-position: -160px -780px; } -.emoji-1F953 { background-position: -180px -780px; } -.emoji-1F954 { background-position: -200px -780px; } -.emoji-1F955 { background-position: -220px -780px; } -.emoji-1F956 { background-position: -240px -780px; } -.emoji-1F957 { background-position: -260px -780px; } -.emoji-1F958 { background-position: -280px -780px; } -.emoji-1F959 { background-position: -300px -780px; } -.emoji-1F95A { background-position: -320px -780px; } -.emoji-1F95B { background-position: -340px -780px; } -.emoji-1F95C { background-position: -360px -780px; } -.emoji-1F95D { background-position: -380px -780px; } -.emoji-1F95E { background-position: -400px -780px; } -.emoji-1F980 { background-position: -420px -780px; } -.emoji-1F981 { background-position: -440px -780px; } -.emoji-1F982 { background-position: -460px -780px; } -.emoji-1F983 { background-position: -480px -780px; } -.emoji-1F984 { background-position: -500px -780px; } -.emoji-1F985 { background-position: -520px -780px; } -.emoji-1F986 { background-position: -540px -780px; } -.emoji-1F987 { background-position: -560px -780px; } -.emoji-1F988 { background-position: -580px -780px; } -.emoji-1F989 { background-position: -600px -780px; } -.emoji-1F98A { background-position: -620px -780px; } -.emoji-1F98B { background-position: -640px -780px; } -.emoji-1F98C { background-position: -660px -780px; } -.emoji-1F98D { background-position: -680px -780px; } -.emoji-1F98E { background-position: -700px -780px; } -.emoji-1F98F { background-position: -720px -780px; } -.emoji-1F990 { background-position: -740px -780px; } -.emoji-1F991 { background-position: -760px -780px; } -.emoji-1F9C0 { background-position: -780px -780px; } -.emoji-203C { background-position: -800px 0; } -.emoji-2049 { background-position: -800px -20px; } -.emoji-2122 { background-position: -800px -40px; } -.emoji-2139 { background-position: -800px -60px; } -.emoji-2194 { background-position: -800px -80px; } -.emoji-2195 { background-position: -800px -100px; } -.emoji-2196 { background-position: -800px -120px; } -.emoji-2197 { background-position: -800px -140px; } -.emoji-2198 { background-position: -800px -160px; } -.emoji-2199 { background-position: -800px -180px; } -.emoji-21A9 { background-position: -800px -200px; } -.emoji-21AA { background-position: -800px -220px; } -.emoji-231A { background-position: -800px -240px; } -.emoji-231B { background-position: -800px -260px; } -.emoji-2328 { background-position: -800px -280px; } -.emoji-23CF { background-position: -800px -300px; } -.emoji-23E9 { background-position: -800px -320px; } -.emoji-23EA { background-position: -800px -340px; } -.emoji-23EB { background-position: -800px -360px; } -.emoji-23EC { background-position: -800px -380px; } -.emoji-23ED { background-position: -800px -400px; } -.emoji-23EE { background-position: -800px -420px; } -.emoji-23EF { background-position: -800px -440px; } -.emoji-23F0 { background-position: -800px -460px; } -.emoji-23F1 { background-position: -800px -480px; } -.emoji-23F2 { background-position: -800px -500px; } -.emoji-23F3 { background-position: -800px -520px; } -.emoji-23F8 { background-position: -800px -540px; } -.emoji-23F9 { background-position: -800px -560px; } -.emoji-23FA { background-position: -800px -580px; } -.emoji-24C2 { background-position: -800px -600px; } -.emoji-25AA { background-position: -800px -620px; } -.emoji-25AB { background-position: -800px -640px; } -.emoji-25B6 { background-position: -800px -660px; } -.emoji-25C0 { background-position: -800px -680px; } -.emoji-25FB { background-position: -800px -700px; } -.emoji-25FC { background-position: -800px -720px; } -.emoji-25FD { background-position: -800px -740px; } -.emoji-25FE { background-position: -800px -760px; } -.emoji-2600 { background-position: -800px -780px; } -.emoji-2601 { background-position: 0 -800px; } -.emoji-2602 { background-position: -20px -800px; } -.emoji-2603 { background-position: -40px -800px; } -.emoji-2604 { background-position: -60px -800px; } -.emoji-260E { background-position: -80px -800px; } -.emoji-2611 { background-position: -100px -800px; } -.emoji-2614 { background-position: -120px -800px; } -.emoji-2615 { background-position: -140px -800px; } -.emoji-2618 { background-position: -160px -800px; } -.emoji-261D { background-position: -180px -800px; } -.emoji-261D-1F3FB { background-position: -200px -800px; } -.emoji-261D-1F3FC { background-position: -220px -800px; } -.emoji-261D-1F3FD { background-position: -240px -800px; } -.emoji-261D-1F3FE { background-position: -260px -800px; } -.emoji-261D-1F3FF { background-position: -280px -800px; } -.emoji-2620 { background-position: -300px -800px; } -.emoji-2622 { background-position: -320px -800px; } -.emoji-2623 { background-position: -340px -800px; } -.emoji-2626 { background-position: -360px -800px; } -.emoji-262A { background-position: -380px -800px; } -.emoji-262E { background-position: -400px -800px; } -.emoji-262F { background-position: -420px -800px; } -.emoji-2638 { background-position: -440px -800px; } -.emoji-2639 { background-position: -460px -800px; } -.emoji-263A { background-position: -480px -800px; } -.emoji-2648 { background-position: -500px -800px; } -.emoji-2649 { background-position: -520px -800px; } -.emoji-264A { background-position: -540px -800px; } -.emoji-264B { background-position: -560px -800px; } -.emoji-264C { background-position: -580px -800px; } -.emoji-264D { background-position: -600px -800px; } -.emoji-264E { background-position: -620px -800px; } -.emoji-264F { background-position: -640px -800px; } -.emoji-2650 { background-position: -660px -800px; } -.emoji-2651 { background-position: -680px -800px; } -.emoji-2652 { background-position: -700px -800px; } -.emoji-2653 { background-position: -720px -800px; } -.emoji-2660 { background-position: -740px -800px; } -.emoji-2663 { background-position: -760px -800px; } -.emoji-2665 { background-position: -780px -800px; } -.emoji-2666 { background-position: -800px -800px; } -.emoji-2668 { background-position: -820px 0; } -.emoji-267B { background-position: -820px -20px; } -.emoji-267F { background-position: -820px -40px; } -.emoji-2692 { background-position: -820px -60px; } -.emoji-2693 { background-position: -820px -80px; } -.emoji-2694 { background-position: -820px -100px; } -.emoji-2696 { background-position: -820px -120px; } -.emoji-2697 { background-position: -820px -140px; } -.emoji-2699 { background-position: -820px -160px; } -.emoji-269B { background-position: -820px -180px; } -.emoji-269C { background-position: -820px -200px; } -.emoji-26A0 { background-position: -820px -220px; } -.emoji-26A1 { background-position: -820px -240px; } -.emoji-26AA { background-position: -820px -260px; } -.emoji-26AB { background-position: -820px -280px; } -.emoji-26B0 { background-position: -820px -300px; } -.emoji-26B1 { background-position: -820px -320px; } -.emoji-26BD { background-position: -820px -340px; } -.emoji-26BE { background-position: -820px -360px; } -.emoji-26C4 { background-position: -820px -380px; } -.emoji-26C5 { background-position: -820px -400px; } -.emoji-26C8 { background-position: -820px -420px; } -.emoji-26CE { background-position: -820px -440px; } -.emoji-26CF { background-position: -820px -460px; } -.emoji-26D1 { background-position: -820px -480px; } -.emoji-26D3 { background-position: -820px -500px; } -.emoji-26D4 { background-position: -820px -520px; } -.emoji-26E9 { background-position: -820px -540px; } -.emoji-26EA { background-position: -820px -560px; } -.emoji-26F0 { background-position: -820px -580px; } -.emoji-26F1 { background-position: -820px -600px; } -.emoji-26F2 { background-position: -820px -620px; } -.emoji-26F3 { background-position: -820px -640px; } -.emoji-26F4 { background-position: -820px -660px; } -.emoji-26F5 { background-position: -820px -680px; } -.emoji-26F7 { background-position: -820px -700px; } -.emoji-26F8 { background-position: -820px -720px; } -.emoji-26F9 { background-position: -820px -740px; } -.emoji-26F9-1F3FB { background-position: -820px -760px; } -.emoji-26F9-1F3FC { background-position: -820px -780px; } -.emoji-26F9-1F3FD { background-position: -820px -800px; } -.emoji-26F9-1F3FE { background-position: 0 -820px; } -.emoji-26F9-1F3FF { background-position: -20px -820px; } -.emoji-26FA { background-position: -40px -820px; } -.emoji-26FD { background-position: -60px -820px; } -.emoji-2702 { background-position: -80px -820px; } -.emoji-2705 { background-position: -100px -820px; } -.emoji-2708 { background-position: -120px -820px; } -.emoji-2709 { background-position: -140px -820px; } -.emoji-270A { background-position: -160px -820px; } -.emoji-270A-1F3FB { background-position: -180px -820px; } -.emoji-270A-1F3FC { background-position: -200px -820px; } -.emoji-270A-1F3FD { background-position: -220px -820px; } -.emoji-270A-1F3FE { background-position: -240px -820px; } -.emoji-270A-1F3FF { background-position: -260px -820px; } -.emoji-270B { background-position: -280px -820px; } -.emoji-270B-1F3FB { background-position: -300px -820px; } -.emoji-270B-1F3FC { background-position: -320px -820px; } -.emoji-270B-1F3FD { background-position: -340px -820px; } -.emoji-270B-1F3FE { background-position: -360px -820px; } -.emoji-270B-1F3FF { background-position: -380px -820px; } -.emoji-270C { background-position: -400px -820px; } -.emoji-270C-1F3FB { background-position: -420px -820px; } -.emoji-270C-1F3FC { background-position: -440px -820px; } -.emoji-270C-1F3FD { background-position: -460px -820px; } -.emoji-270C-1F3FE { background-position: -480px -820px; } -.emoji-270C-1F3FF { background-position: -500px -820px; } -.emoji-270D { background-position: -520px -820px; } -.emoji-270D-1F3FB { background-position: -540px -820px; } -.emoji-270D-1F3FC { background-position: -560px -820px; } -.emoji-270D-1F3FD { background-position: -580px -820px; } -.emoji-270D-1F3FE { background-position: -600px -820px; } -.emoji-270D-1F3FF { background-position: -620px -820px; } -.emoji-270F { background-position: -640px -820px; } -.emoji-2712 { background-position: -660px -820px; } -.emoji-2714 { background-position: -680px -820px; } -.emoji-2716 { background-position: -700px -820px; } -.emoji-271D { background-position: -720px -820px; } -.emoji-2721 { background-position: -740px -820px; } -.emoji-2728 { background-position: -760px -820px; } -.emoji-2733 { background-position: -780px -820px; } -.emoji-2734 { background-position: -800px -820px; } -.emoji-2744 { background-position: -820px -820px; } -.emoji-2747 { background-position: -840px 0; } -.emoji-274C { background-position: -840px -20px; } -.emoji-274E { background-position: -840px -40px; } -.emoji-2753 { background-position: -840px -60px; } -.emoji-2754 { background-position: -840px -80px; } -.emoji-2755 { background-position: -840px -100px; } -.emoji-2757 { background-position: -840px -120px; } -.emoji-2763 { background-position: -840px -140px; } -.emoji-2764 { background-position: -840px -160px; } -.emoji-2795 { background-position: -840px -180px; } -.emoji-2796 { background-position: -840px -200px; } -.emoji-2797 { background-position: -840px -220px; } -.emoji-27A1 { background-position: -840px -240px; } -.emoji-27B0 { background-position: -840px -260px; } -.emoji-27BF { background-position: -840px -280px; } -.emoji-2934 { background-position: -840px -300px; } -.emoji-2935 { background-position: -840px -320px; } -.emoji-2B05 { background-position: -840px -340px; } -.emoji-2B06 { background-position: -840px -360px; } -.emoji-2B07 { background-position: -840px -380px; } -.emoji-2B1B { background-position: -840px -400px; } -.emoji-2B1C { background-position: -840px -420px; } -.emoji-2B50 { background-position: -840px -440px; } -.emoji-2B55 { background-position: -840px -460px; } -.emoji-3030 { background-position: -840px -480px; } -.emoji-303D { background-position: -840px -500px; } -.emoji-3297 { background-position: -840px -520px; } -.emoji-3299 { background-position: -840px -540px; } - -.emoji-icon { - background-image: image-url('emoji.png'); - background-repeat: no-repeat; - height: 20px; - width: 20px; - - @media only screen and (-webkit-min-device-pixel-ratio: 2), - only screen and (min--moz-device-pixel-ratio: 2), - only screen and (-o-min-device-pixel-ratio: 2/1), - only screen and (min-device-pixel-ratio: 2), - only screen and (min-resolution: 192dpi), - only screen and (min-resolution: 2dppx) { - background-image: image-url('emoji@2x.png'); - background-size: 860px 840px; - } +gl-emoji { + display: inline-block; + display: inline-flex; + vertical-align: middle; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 1.5em; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 30f242a35db..ffece53a093 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -271,6 +271,7 @@ span.idiff { font-size: 13px; line-height: 28px; display: inline-block; + float: none; } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e3da467a27c..51805c5d734 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -1,10 +1,24 @@ .filter-item { - margin-right: 6px; vertical-align: top; &.reset-filters { padding: 7px; } + + &.update-issues-btn { + float: right; + margin-right: 0; + + @media (max-width: $screen-xs-max) { + float: none; + } + } +} + +.filters-section { + @media (max-width: $screen-xs-max) { + display: inline-block; + } } @media (min-width: $screen-sm-min) { @@ -14,6 +28,20 @@ width: 132px; } } + + .filter-item:not(:last-child) { + margin-right: 6px; + } + + .sort-filter { + display: inline-block; + float: right; + } + + .dropdown-menu-sort { + left: auto; + right: 0; + } } @media (max-width: $screen-xs-max) { @@ -21,11 +49,106 @@ display: block; margin: 0 0 10px; } + + .dropdown-menu-toggle, + .update-issues-btn .btn { + width: 100%; + } } .filtered-search-container { display: -webkit-flex; display: flex; + + @media (max-width: $screen-xs-min) { + -webkit-flex-direction: column; + flex-direction: column; + } + + .tokens-container { + display: -webkit-flex; + display: flex; + flex: 1; + -webkit-flex: 1; + padding-left: 30px; + position: relative; + margin-bottom: 0; + } + + .input-token { + max-width: 200px; + } + + .input-token:only-child, + .input-token:last-child { + flex: 1; + -webkit-flex: 1; + max-width: initial; + } +} + +.filtered-search-token, +.filtered-search-term { + display: -webkit-flex; + display: flex; + margin-top: 5px; + margin-bottom: 5px; + + .selectable { + display: -webkit-flex; + display: flex; + } + + .name, + .value { + display: inline-block; + padding: 2px 7px; + } + + .name { + background-color: $filter-name-resting-color; + color: $filter-name-text-color; + border-radius: 2px 0 0 2px; + margin-right: 1px; + text-transform: capitalize; + } + + .value { + background-color: $white-normal; + color: $filter-value-text-color; + border-radius: 0 2px 2px 0; + margin-right: 5px; + } + + .selected { + .name { + background-color: $filter-name-selected-color; + } + + .value { + background-color: $filter-value-selected-color; + } + } +} + +.filtered-search-term { + .name { + background-color: inherit; + color: $black; + text-transform: none; + } + + .selectable { + cursor: text; + } +} + +.scroll-container { + display: -webkit-flex; + display: flex; + overflow-x: auto; + white-space: nowrap; + width: 100%; } .filtered-search-input-container { @@ -33,13 +156,48 @@ display: flex; position: relative; width: 100%; + border: 1px solid $border-color; + background-color: $white-light; + + @media (max-width: $screen-xs-min) { + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + margin-bottom: 10px; + + .dropdown-menu { + width: auto; + left: 0; + right: 0; + max-width: none; + min-width: 100%; + } + } + + &:hover { + @extend .form-control:hover; + } + + &.focus, + &.focus:hover { + border-color: $dropdown-input-focus-border; + box-shadow: 0 0 4px $search-input-focus-shadow-color; + } + + &.focus .fa-filter { + color: $common-gray-dark; + } .form-control { - padding-left: 25px; - padding-right: 25px; + position: relative; + min-width: 200px; + padding: 5px 25px 6px 0; + border-color: transparent; - &:focus ~ .fa-filter { - color: $common-gray-dark; + &:focus, + &:hover { + outline: none; + border-color: transparent; + box-shadow: none; } } @@ -57,12 +215,13 @@ .clear-search { width: 35px; - background-color: transparent; + background-color: $white-light; border: none; position: absolute; right: 0; height: 100%; outline: none; + z-index: 1; &:hover .fa-times { color: $common-gray-dark; @@ -70,6 +229,15 @@ } } +.filter-dropdown-container { + display: -webkit-flex; + display: flex; + + .dropdown-toggle { + line-height: 22px; + } +} + .dropdown-menu .filter-dropdown-item { padding: 0; } @@ -79,6 +247,41 @@ overflow: auto; } +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + .issues-details-filters { + .dropdown-menu-toggle { + width: 100px; + } + } +} + +@media (max-width: $screen-xs-max) { + .issues-details-filters { + padding: 0 0 10px; + background-color: $white-light; + border-top: 0; + } +} + +@media (max-width: $screen-xs) { + .filter-dropdown-container { + .dropdown-toggle, + .dropdown { + width: 100%; + } + + .dropdown { + margin-left: 0; + } + + .fa-chevron-down { + position: absolute; + right: 10px; + top: 10px; + } + } +} + %filter-dropdown-item-btn-hover { background-color: $dropdown-hover-color; color: $white-light; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 78434b99b62..6660a022260 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -149,14 +149,14 @@ header { .header-logo { display: inline-block; - margin: 0 8px 0 3px; + margin: 0 7px 0 2px; position: relative; - top: 7px; + top: 10px; transition-duration: .3s; svg, img { - height: 36px; + height: 28px; } &:hover { @@ -164,12 +164,25 @@ header { } } + .group-name-toggle { + margin: 0 5px; + vertical-align: sub; + } + + .group-title { + &.is-hidden { + .hidable:not(:last-of-type) { + display: none; + } + } + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; - max-width: 450px; + max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -179,10 +192,26 @@ header { vertical-align: top; white-space: nowrap; + &.initializing { + display: none; + } + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + @media (max-width: $screen-xs-max) { max-width: 190px; } + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + max-width: 428px; + } + + @media (min-width: $screen-lg-min) { + max-width: 685px; + } + a { color: $gl-text-color; @@ -253,24 +282,34 @@ header { font-size: 18px; .navbar-nav { + display: table; + table-layout: fixed; + width: 100%; margin: 0; - float: none !important; - - .visible-xs, - .visible-sm { - display: table-cell !important; - } + text-align: right; } .navbar-collapse { padding-left: 5px; - .nav > li { - display: table-cell; - width: 1%; + .nav > li:not(.hidden-xs) { + display: table-cell!important; + width: 25%; + + a { + margin-right: 8px; + } } } } + + .header-user-dropdown-toggle { + text-align: center; + } + + .header-user-avatar { + float: none; + } } .header-user { diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 909a0f4afda..6d27d7568cf 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -57,8 +57,13 @@ visibility: hidden; } - &:hover i { - visibility: visible; + &:hover, + &:focus { + outline: none; + + & i { + visibility: visible; + } } } } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index d335fedefe2..300ba4f2de6 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,17 +2,6 @@ font-family: $regular_font; font-size: $font-size-base; - &.ui-autocomplete { - border-color: $jq-ui-border; - padding: 0; - margin-top: 2px; - z-index: 1001; - - .ui-menu-item a { - padding: 4px 10px; - } - } - .ui-state-default { border: 1px solid $white-light; background: $white-light; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 29d55c44699..0a42b17c1f5 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -8,6 +8,19 @@ body { &.navless { background-color: $white-light !important; } + + &.card-content { + background-color: $gray-darker; + + .content-wrapper { + padding: 0; + + .container-fluid, + .container-limited { + background-color: $gray-darker; + } + } + } } .container { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 2bfdb9f9601..7adbb0a4188 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -96,16 +96,6 @@ ul.unstyled-list > li { border-bottom: none; } -ul.task-list { - li.task-list-item { - list-style-type: none; - } - - ul:not(.task-list) { - padding-left: 1.3em; - } -} - // Generic content list ul.content-list { @include basic-list; @@ -239,44 +229,6 @@ ul.content-list { } } -// Table list -.table-list { - display: table; - width: 100%; - - .table-list-row { - display: table-row; - } - - .table-list-cell { - display: table-cell; - vertical-align: top; - padding: 10px 16px; - border-bottom: 1px solid $gray-darker; - - &.avatar-cell { - width: 36px; - padding-right: 0; - - img { - margin-right: 0; - } - } - } - - &.table-wide { - .table-list-cell { - &:last-of-type { - padding-right: 0; - } - - &:first-of-type { - padding-left: 0; - } - } - } -} - .panel > .content-list > li { padding: $gl-padding-top $gl-padding; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index d4758d90352..a668a6c4c39 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -147,6 +147,9 @@ } .atwho-view { + overflow-y: auto; + overflow-x: hidden; + small.description { float: right; padding: 3px 5px; @@ -162,4 +165,8 @@ @include disableAllAnimation; } } + + ul > li { + white-space: nowrap; + } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1acd06122a3..df78bbdea51 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -76,6 +76,13 @@ #{$property}: $value; } +/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */ +@mixin on-webkit-only { + @media screen and (-webkit-min-device-pixel-ratio:0) { + @content; + } +} + @mixin keyframes($animation-name) { @-webkit-keyframes #{$animation-name} { @content; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 8e2c56a8488..eb73f7cc794 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -100,8 +100,7 @@ @media (max-width: $screen-sm-max) { .issues-filters { - .milestone-filter, - .labels-filter { + .milestone-filter { display: none; } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7d4a814a36c..205d23b1329 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,7 +1,7 @@ @mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; - z-index: 1; + z-index: 2; position: absolute; bottom: 12px; width: 43px; @@ -18,7 +18,7 @@ .fa { position: relative; - top: 6px; + top: 5px; font-size: 18px; } } @@ -79,6 +79,7 @@ } &.sub-nav { + text-align: center; background-color: $gray-normal; .container-fluid { @@ -137,7 +138,6 @@ .nav-links { display: inline-block; - width: 50%; margin-bottom: 0; border-bottom: none; @@ -286,13 +286,14 @@ background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; + text-align: center; .container-fluid { position: relative; .nav-control { @media (max-width: $screen-sm-max) { - margin-right: 75px; + margin-right: 2px; } } } @@ -351,7 +352,7 @@ right: -5px; .fa { - right: -28px; + right: -7px; } } @@ -381,7 +382,7 @@ left: 0; .fa { - left: -4px; + left: 10px; } } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index efe93724013..9d8d08dff88 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -48,11 +48,3 @@ line-height: inherit; } } - -.panel-default { - .table-list-row:last-child { - .table-list-cell { - border-bottom: 0; - } - } -} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d09b1c9d7f5..40e93032f59 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -55,7 +55,7 @@ padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - &:not(.build-sidebar):not(.wiki-sidebar) { + .content-wrapper { padding-right: $gutter_collapsed_width; } } @@ -73,10 +73,6 @@ right: $gutter_collapsed_width; } } - - &.with-overlay { - padding-right: $gutter_collapsed_width; - } } .right-sidebar { diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index ea2d26dd5a0..12a86a64645 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -86,6 +86,16 @@ position: fixed; } +/* + * Fix <summary> elements on firefox + * See https://github.com/necolas/normalize.css/issues/640 + * and https://github.com/twbs/bootstrap/issues/21060 + * + */ +summary { + display: list-item; +} + @import "bootstrap/responsive-utilities"; // Labels diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 54958973f15..c241816788b 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -134,7 +134,7 @@ ul, ol { padding: 0; - margin: 3px 0 3px 28px !important; + margin: 3px 0 !important; } ul:dir(rtl), @@ -144,6 +144,29 @@ li { line-height: 1.6em; + margin-left: 25px; + padding-left: 3px; + + /* Normalize the bullet position on webkit. */ + @include on-webkit-only { + margin-left: 28px; + padding-left: 0; + } + } + + ul.task-list { + li.task-list-item { + list-style-type: none; + position: relative; + padding-left: 28px; + margin-left: 0 !important; + + input.task-list-item-checkbox { + position: absolute; + left: 8px; + top: 5px; + } + } } a[href*="/uploads/"], @@ -283,6 +306,11 @@ a > code { * Textareas intended for GFM * */ +textarea.js-gfm-input { + font-family: $monospace_font; + font-size: 13px; +} + .strikethrough { text-decoration: line-through; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ba0af072716..6841adb637e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1; * Fonts */ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; /* * Dropdowns @@ -540,3 +540,12 @@ Pipeline Graph $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; + +/* +Filtered Search +*/ +$filter-name-resting-color: #f8f8f8; +$filter-name-text-color: rgba(0, 0, 0, 0.55); +$filter-value-text-color: rgba(0, 0, 0, 0.85); +$filter-name-selected-color: #ebebeb; +$filter-value-selected-color: #d7d7d7; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 97ade638db6..0c226ff7598 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -20,8 +20,9 @@ outline: none; resize: none; height: 100vh; + max-height: calc(100vh - 10px); max-width: 900px; - margin: 0 auto; + margin: 0 auto 10px; } .zen-control-leave { diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 6f2e746d4b0..09951fe3d3e 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -20,6 +20,8 @@ $dark-highlight-bg: #ffe792; $dark-highlight-color: $black; $dark-pre-hll-bg: #373b41; $dark-hll-bg: #373b41; +$dark-over-bg: #9f9ab5; +$dark-expanded-bg: #3e3e3e; $dark-c: #969896; $dark-err: #c66; $dark-k: #b294bb; @@ -139,9 +141,37 @@ $dark-il: #de935f; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $dark-over-bg; + border-color: darken($dark-over-bg, 5%); + + a { + color: darken($dark-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $dark-expanded-bg; + border-color: $dark-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 2144a5f7466..b6a6d298adf 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -13,6 +13,8 @@ $monokai-line-empty-bg: #49483e; $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-diff-border: #808080; $monokai-highlight-bg: #ffe792; +$monokai-over-bg: #9f9ab5; +$monokai-expanded-bg: #3e3e3e; $monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-idiff: rgba(166, 226, 46, 0.15); @@ -139,9 +141,37 @@ $monokai-gi: #a6e22e; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $monokai-over-bg; + border-color: darken($monokai-over-bg, 5%); + + a { + color: darken($monokai-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $monokai-expanded-bg; + border-color: $monokai-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 2cb1d18f12f..4f7a50dcb4f 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -17,6 +17,8 @@ $solarized-dark-line-color-new: #5a766c; $solarized-dark-line-color-old: #7a6c71; $solarized-dark-highlight: #094554; $solarized-dark-hll-bg: #174652; +$solarized-dark-over-bg: #9f9ab5; +$solarized-dark-expanded-bg: #010d10; $solarized-dark-c: #586e75; $solarized-dark-err: #93a1a1; $solarized-dark-g: #93a1a1; @@ -143,9 +145,37 @@ $solarized-dark-il: #2aa198; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $solarized-dark-over-bg; + border-color: darken($solarized-dark-over-bg, 5%); + + a { + color: darken($solarized-dark-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $black; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $solarized-dark-expanded-bg; + border-color: $solarized-dark-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index b72c4326730..6463fe96c1b 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -18,6 +18,9 @@ $solarized-light-line-color-new: #a1a080; $solarized-light-line-color-old: #ad9186; $solarized-light-highlight: #eee8d5; $solarized-light-hll-bg: #ddd8c5; +$solarized-light-over-bg: #ded7fc; +$solarized-light-expanded-border: #d2cdbd; +$solarized-light-expanded-bg: #ece6d4; $solarized-light-c: #93a1a1; $solarized-light-err: #586e75; $solarized-light-g: #586e75; @@ -150,9 +153,37 @@ $solarized-light-il: #2aa198; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $solarized-light-over-bg; + border-color: darken($solarized-light-over-bg, 5%); + + a { + color: darken($solarized-light-over-bg, 15%); + } + } + } + .line_content.match { @include matchLine; } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $solarized-light-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $solarized-light-expanded-bg; + border-color: $solarized-light-expanded-bg; + } + } } // highlight line via anchor diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 398fbfd3b18..ab2018bfbca 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -7,6 +7,9 @@ $white-code-color: $gl-text-color; $white-highlight: #fafe3d; $white-pre-hll-bg: #f8eec7; $white-hll-bg: #f8f8f8; +$white-over-bg: #ded7fc; +$white-expanded-border: #e0e0e0; +$white-expanded-bg: #f7f7f7; $white-c: #998; $white-err: #a61717; $white-err-bg: #e3d2d2; @@ -123,12 +126,38 @@ $white-gc-bg: #eaf2f5; } } + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $white-over-bg; + border-color: darken($white-over-bg, 5%); + + a { + color: darken($white-over-bg, 15%); + } + } + &.hll:not(.empty-cell) { background-color: $line-number-select; border-color: $line-select-yellow-dark; } } + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $white-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $white-expanded-bg; + border-color: $white-expanded-bg; + } + } + .line_content { &.old { background-color: $line-removed; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9a36d76136b..f9ee33019cd 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -420,12 +420,9 @@ display: -webkit-flex; display: flex; - .form-control { - margin-left: auto; - - @media (min-width: $screen-sm-min) { - max-width: 200px; - } + .issues-filters { + -webkit-flex: 1; + flex: 1; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c3d45d708c1..da8410eca66 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -38,6 +38,38 @@ } } +.pipeline-info { + .status-icon-container { + display: inline-block; + vertical-align: middle; + margin-right: 3px; + + svg { + display: block; + width: 22px; + height: 22px; + } + } + + .mr-widget-pipeline-graph { + display: inline-block; + vertical-align: middle; + margin: 0 -6px 0 0; + + .dropdown-menu { + margin-top: 11px; + } + } +} + +.branch-info .commit-icon { + margin-right: 3px; + + svg { + top: 3px; + } +} + /* * Commit message textarea for web editor and * custom merge request message @@ -78,6 +110,7 @@ padding: 5px 10px; background-color: $gray-light; border-bottom: 1px solid $gray-darker; + border-top: 1px solid $gray-darker; font-size: 14px; &:first-child { @@ -117,10 +150,37 @@ } } +.commit.flex-list { + display: flex; +} + +.avatar-cell { + width: 46px; + padding-left: 10px; + + img { + margin-right: 0; + } +} + +.commit-detail { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-grow: 1; + padding-left: 10px; + + .merge-request-branches & { + flex-direction: column; + } +} + +.commit-content { + padding-right: 10px; +} + .commit-actions { @media (min-width: $screen-sm-min) { - width: 300px; - text-align: right; font-size: 0; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 5b777953fb0..ad3dbc7ac48 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -369,13 +369,11 @@ // Custom CSS for components .item-conmmit-component { .commit-icon { - position: relative; - top: 3px; - left: 1px; - display: inline-block; - svg { - float: left; + display: inline-block; + width: 20px; + height: 20px; + vertical-align: bottom; } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 92d7772da57..eab79c2a481 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -89,6 +89,10 @@ .diff-line-num { width: 50px; + + a { + transition: none; + } } .line_holder td { @@ -111,7 +115,7 @@ } .add-diff-note { - margin-left: -65px; + margin-left: -55px; } } @@ -133,8 +137,13 @@ width: 35px; font-weight: normal; - &:hover { - text-decoration: underline; + &[disabled] { + cursor: default; + + &:hover, + &:active { + text-decoration: none; + } } } } @@ -485,3 +494,103 @@ } } } + +.diff-comment-avatar-holders { + position: absolute; + height: 19px; + width: 19px; + margin-left: -15px; + + &:hover { + .diff-comment-avatar, + .diff-comments-more-count { + @for $i from 1 through 4 { + $x-pos: 14px; + + &:nth-child(#{$i}) { + @if $i == 4 { + $x-pos: 14.5px; + } + + transform: translateX((($i * $x-pos) - $x-pos)); + + &:hover { + transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + } + } + } + } + + .diff-comments-more-count { + padding-left: 2px; + padding-right: 2px; + width: auto; + } + } +} + +.diff-comment-avatar, +.diff-comments-more-count { + position: absolute; + left: 0; + width: 19px; + height: 19px; + margin-right: 0; + border-color: $white-light; + cursor: pointer; + transition: all .1s ease-out; + + @for $i from 1 through 4 { + &:nth-child(#{$i}) { + z-index: (4 - $i); + } + } +} + +.diff-comments-more-count { + width: 19px; + min-width: 19px; + padding-left: 0; + padding-right: 0; + overflow: hidden; +} + +.diff-comments-more-count, +.diff-notes-collapse { + background-color: $gray-darkest; + color: $white-light; + border: 1px solid $white-light; + border-radius: 1em; + font-family: $regular_font; + font-size: 9px; + line-height: 17px; + text-align: center; +} + +.diff-notes-collapse { + position: relative; + width: 19px; + height: 19px; + padding: 0; + transition: transform .1s ease-out; + + svg { + position: absolute; + left: 50%; + top: 50%; + margin-left: -5.5px; + margin-top: -5.5px; + } + + path { + fill: $white-light; + } + + &:hover { + transform: scale(1.2); + } + + &:focus { + outline: 0; + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 181dcb7721f..73a5da715f2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -15,96 +15,97 @@ padding-top: 20px; } -@media (max-width: $screen-xs-max) { - .environments-container { +.environments-container { + .table-holder { width: 100%; overflow: auto; } -} - -.environments { - table-layout: fixed; - .environments-commit, - .environments-actions, - .environments-deploy, - .environments-build, - .environments-date { - position: static; - float: none; - display: table-cell; - } + .table.ci-table { + .environments-actions { + min-width: 200px; + } - .environments-name, - .environments-commit, - .environments-actions { - width: 20%; - } + .environments-commit, + .environments-actions { + width: 20%; + } - .environments-date { - width: 10%; - } + .environments-date { + width: 10%; + } - .environments-deploy, - .environments-build { - width: 15%; - } + .environments-name, + .environments-deploy, + .environments-build { + width: 15%; + } - .environment-name, - .environments-build-cell, - .deployment-column { - word-break: break-all; - } + .deployment-column { + > span { + word-break: break-all; + } - .deployment-column { - .avatar { - float: none; + .avatar { + float: none; + } } - } - .commit-title { - margin: 0; - } + .btn-group { - .avatar-image-container { - text-decoration: none; - } + > a { + color: $gl-text-color-secondary; + } - .icon-play { - height: 13px; - width: 12px; - } + svg path { + fill: $gl-text-color-secondary; + } - .external-url, - .dropdown-new { - color: $gl-text-color-secondary; - } + .dropdown { + outline: none; + } + } - .dropdown-menu { + .commit-title { + margin: 0; + } - .fa { - margin-right: 6px; - color: $gl-text-color-secondary; + .avatar-image-container { + text-decoration: none; } - } - .build-link, - .branch-name { - color: $gl-text-color; - } + .icon-play { + height: 13px; + width: 12px; + } - .stop-env-link, - .external-url { - color: $gl-text-color-secondary; + .external-url, + .dropdown-new { + color: $gl-text-color-secondary; + } - .stop-env-icon { - font-size: 14px; + .dropdown-menu { + .fa { + margin-right: 6px; + color: $gl-text-color-secondary; + } } - } - .deployment { - .build-column { + .build-link, + .branch-name { + color: $gl-text-color; + } + + .stop-env-link, + .external-url { + color: $gl-text-color-secondary; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment .build-column { .build-link { color: $gl-text-color; } @@ -113,34 +114,108 @@ float: none; } } - } - - .folder-icon { - margin-right: 3px; - color: $gl-text-color-secondary; - display: inline-block; - .fa:nth-child(1) { + .folder-icon { margin-right: 3px; + color: $gl-text-color-secondary; + display: inline-block; + + .fa:nth-child(1) { + margin-right: 3px; + } + } + + .folder-name { + cursor: pointer; + color: $gl-text-color-secondary; + display: inline-block; } - } - .folder-name { - cursor: pointer; - color: $gl-text-color-secondary; - display: inline-block; + .icon-container { + width: 20px; + text-align: center; + } + + .branch-commit { + .commit-id { + margin-right: 0; + } + } + + .no-btn { + border: none; + background: none; + outline: none; + width: 100%; + text-align: left; + } } } -.table.ci-table.environments { - .icon-container { - width: 20px; - text-align: center; +.prometheus-graph { + text { + fill: $stat-graph-axis-fill; } +} - .branch-commit { - .commit-id { - margin-right: 0; - } +.x-axis path, +.y-axis path, +.label-x-axis-line, +.label-y-axis-line { + fill: none; + stroke-width: 1; + shape-rendering: crispEdges; +} + +.x-axis path, +.y-axis path { + stroke: $stat-graph-axis-fill; +} + +.label-x-axis-line, +.label-y-axis-line { + stroke: $border-color; +} + +.y-axis { + line { + stroke: $stat-graph-axis-fill; + stroke-width: 1; } } + +.metric-area { + opacity: 0.8; +} + +.prometheus-graph-overlay { + fill: none; + opacity: 0.0; + pointer-events: all; +} + +.rect-text-metric { + fill: $white-light; + stroke-width: 1; + stroke: $black; +} + +.rect-axis-text { + fill: $white-light; +} + +.text-metric, +.text-median-metric, +.text-metric-usage, +.text-metric-date { + fill: $black; +} + +.text-metric-date { + font-weight: 200; +} + +.selected-metric-line { + stroke: $black; + stroke-width: 1; +} diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5776d86983a..08398bb43a2 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -155,7 +155,7 @@ @media (max-width: $screen-xs-max) { .event-item { - padding-left: $gl-padding; + padding-left: 0; .event-title { white-space: normal; @@ -169,8 +169,7 @@ .event-body { margin: 0; - border-left: 2px solid $events-body-border; - padding-left: 10px; + padding-left: 0; } .event-item-timestamp { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index d377526e655..84d21e48463 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -73,3 +73,19 @@ } } } + +.mattermost-icon svg { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +.mattermost-team-name { + color: $gl-text-color-secondary; +} + +.mattermost-info { + display: block; + color: $gl-text-color-secondary; + margin-top: 10px; +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 80b0c9493d8..cb7ebd61504 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -10,6 +10,11 @@ .issue-labels { display: inline-block; } + + .icon-merge-request-unmerged { + height: 13px; + margin-bottom: 3px; + } } } @@ -51,7 +56,10 @@ ul.related-merge-requests > li { .merge-request-id { display: inline-block; - width: 3em; +} + +.merge-request-info { + margin-left: 5px; } .merge-request-status { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0b0c4bc130d..7c3172421c1 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,6 @@ * */ .mr-state-widget { - background: $gray-light; color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -25,11 +24,7 @@ color: inherit; } - .btn-success.dropdown-toggle:disabled { - background-color: $gl-success; - } - - .accept_merge_request { + .accept-merge-request { &.ci-pending, &.ci-running { @include btn-blue; @@ -42,6 +37,12 @@ @include btn-red; } } + + .dropdown-toggle { + .fa { + color: inherit; + } + } } .accept-control { @@ -103,12 +104,17 @@ @media (max-width: $screen-xs-max) { flex-wrap: wrap; } + + .ci-status-icon > .icon-link > svg { + width: 22px; + height: 22px; + } } .mr-widget-body, .ci_widget, .mr-widget-footer { - padding: $gl-padding; + padding: 16px; } .mr-widget-pipeline-graph { @@ -168,10 +174,6 @@ } } - p:last-child { - margin-bottom: 0; - } - .btn-grouped { margin-left: 0; margin-right: 7px; @@ -234,8 +236,7 @@ .commit { margin: 0; - padding-top: 2px; - padding-bottom: 2px; + padding: 10px 0; list-style: none; &:hover { @@ -334,8 +335,61 @@ } } +.remove-message-pipes { + ul { + margin: 10px 0 0 12px; + padding: 0; + list-style: none; + border-left: 2px solid $border-color; + display: inline-block; + } + + li { + position: relative; + margin: 0; + padding: 0; + display: block; + + span { + margin-left: 15px; + max-height: 20px; + } + } + + li::before { + content: ''; + position: absolute; + border-top: 2px solid $border-color; + height: 1px; + top: 8px; + width: 8px; + } + + li:last-child { + &::before { + top: 18px; + } + + span { + display: block; + position: relative; + top: 5px; + margin-top: 5px; + } + } +} + .mr-source-target { + background-color: $gray-light; line-height: 31px; + border-style: solid; + border-width: 1px; + border-color: $border-color; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + border-bottom: none; + padding: 16px; + margin-bottom: -1px; } .panel-new-merge-request { @@ -350,7 +404,7 @@ } .panel-footer { - padding: 5px 10px; + padding: 0; .btn { min-width: auto; @@ -420,6 +474,11 @@ } } +.assign-to-me-link { + padding-left: 12px; + white-space: nowrap; +} + .table-holder { .ci-table { @@ -431,6 +490,8 @@ } .merged-buttons { + margin-top: 20px; + .btn { float: left; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index f984b469609..927bf9805ce 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -148,6 +148,18 @@ .error-alert > .alert { margin-top: 5px; margin-bottom: 5px; + + &.alert-dismissable { + .close { + color: $white-light; + opacity: 0.85; + font-weight: normal; + + &:hover { + opacity: 1; + } + } + } } .discussion-body, @@ -178,8 +190,25 @@ padding-right: 5px; } - &:last-child { - padding-left: 5px; + } + + .discussion-actions { + display: table; + + .new-issue-for-discussion path { + fill: $gray-darkest; + } + + .btn-group { + display: table-cell; + + &:first-child { + padding-right: 0; + } + + &:first-child:not(:last-child) > div { + border-right: 0; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index aa130a1abb0..e238f0865f6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -331,6 +331,10 @@ ul.notes { &:hover { color: $gl-link-color; + } + + &:focus, + &:hover { text-decoration: none; } } @@ -380,7 +384,7 @@ ul.notes { top: 0; .note-action-button { - margin-left: 10px; + margin-left: 8px; } } @@ -396,8 +400,7 @@ ul.notes { } .note-action-button { - display: inline-block; - margin-left: 0; + display: inline; line-height: 20px; @media (min-width: $screen-sm-min) { @@ -452,36 +455,37 @@ ul.notes { * Line note button on the side of diffs */ -.diff-file tr.line_holder { - @mixin show-add-diff-note { - display: inline-block; - } +.add-diff-note { + display: none; + margin-top: -2px; + border-radius: 50%; + background: $white-light; + padding: 1px 5px; + font-size: 12px; + color: $gl-link-color; + margin-left: -55px; + position: absolute; + z-index: 10; + width: 23px; + height: 23px; + border: 1px solid $border-color; + transition: transform .1s ease-in-out; - .add-diff-note { - margin-top: -8px; - border-radius: 40px; - background: $white-light; - padding: 4px; - font-size: 16px; - color: $gl-link-color; - margin-left: -56px; - position: absolute; - z-index: 10; - width: 32px; - // "hide" it by default - display: none; + &:hover { + background: $gl-info; + color: $white-light; + transform: scale(1.15); + } - &:hover { - background: $gl-info; - color: $white-light; - @include show-add-diff-note; - } + &:active { + outline: 0; } +} - // "show" the icon also if we just hover somewhere over the line - &:hover > td { +.diff-file { + .is-over { .add-diff-note { - @include show-add-diff-note; + display: inline-block; } } } @@ -505,6 +509,7 @@ ul.notes { } .line-resolve-all-container { + .btn-group { margin-left: -4px; } @@ -513,6 +518,27 @@ ul.notes { border-top-left-radius: 0; border-bottom-left-radius: 0; } + + .btn.discussion-create-issue-btn { + margin-left: -4px; + border-radius: 0; + border-right: 0; + + a { + padding: 0; + line-height: 0; + + &:hover { + text-decoration: none; + border: 0; + } + } + + .new-issue-for-discussion path { + fill: $gray-darkest; + } + } + } .line-resolve-all { @@ -535,7 +561,6 @@ ul.notes { } .line-resolve-btn { - display: inline-block; position: relative; top: 2px; padding: 0; @@ -558,8 +583,9 @@ ul.notes { } svg { - position: relative; fill: $gray-darkest; + height: 15px; + width: 15px; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 3fe1eef307e..33b38ca6923 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -13,21 +13,16 @@ white-space: nowrap; } - .commit-title { - margin: 0; - } - - .controls { - white-space: nowrap; + .table-holder { + width: 100%; + overflow: auto; } - .btn { - margin: 4px; + .commit-title { + margin: 0; } .table.ci-table { - min-width: 1200px; - table-layout: fixed; .label { margin-bottom: 3px; @@ -37,16 +32,67 @@ color: $black; } - .pipeline-date, - .pipeline-status { - width: 10%; + .stage-cell { + min-width: 130px; // Guarantees we show at least 4 stages in line + width: 20%; + } + + .pipelines-time-ago { + text-align: right; } - .pipeline-info, - .pipeline-commit, - .pipeline-stages, .pipeline-actions { - width: 20%; + padding-right: 0; + min-width: 170px; //Guarantees buttons don't break in several lines. + + .btn-default { + color: $gl-text-color-secondary; + } + + .btn.btn-retry:hover, + .btn.btn-retry:focus { + border-color: $gray-darkest; + background-color: $white-normal; + } + + svg path { + fill: $gl-text-color-secondary; + } + + .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } + + .dropdown-toggle, + .dropdown-menu { + color: $gl-text-color-secondary; + + .fa { + color: $gl-text-color-secondary; + font-size: 14px; + } + } + + .btn-group { + &.open { + .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; + } + } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } + } + + .tooltip { + white-space: nowrap; + } } } } @@ -54,6 +100,7 @@ @media (max-width: $screen-md-max) { .content-list { &.pipelines, + &.environments-container, &.builds-content-list { width: 100%; overflow: auto; @@ -61,27 +108,10 @@ } } -.content-list.pipelines .table-holder { - min-height: 300px; -} - -.pipeline-holder { - width: 100%; - overflow: auto; -} - .table.ci-table { - min-width: 900px; - &.pipeline { - min-width: 650px; - } - - &.builds-page { - - tr { - height: 71px; - } + &.builds-page tbody tr { + height: 71px; } tr { @@ -94,12 +124,16 @@ padding: 10px 8px; } + td.environments-actions { + padding-right: 0; + } + td.stage-cell { padding: 10px 0; } .commit-link { - padding: 9px 8px 10px; + padding: 9px 8px 10px 2px; } } @@ -206,72 +240,8 @@ } } - .pipeline-actions { - min-width: 140px; - - .btn { - margin: 0; - color: $gl-text-color-secondary; - } - - .cancel-retry-btns { - vertical-align: middle; - - .btn:not(:first-child) { - margin-left: 8px; - } - } - - .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } - - .dropdown-toggle, - .dropdown-menu { - color: $gl-text-color-secondary; - - .fa { - color: $gl-text-color-secondary; - font-size: 14px; - } - - svg, - .fa { - margin-right: 0; - } - } - - .btn-remove { - color: $white-light; - } - - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } - - .btn { - .icon-play { - height: 13px; - width: 12px; - } - } - } - - .tooltip { - white-space: nowrap; - } - } - - .build-link { - - a { - color: $gl-text-color; - } + .build-link a { + color: $gl-text-color; } .btn-group.open .dropdown-toggle { @@ -335,31 +305,8 @@ } .tab-pane { - &.pipelines { - .ci-table { - min-width: 900px; - } - - .content-list.pipelines { - overflow: auto; - } - - .stage { - max-width: 100px; - width: 100px; - } - - .pipeline-actions { - min-width: initial; - } - } - - &.builds { - .ci-table { - tr { - height: 71px; - } - } + &.builds .ci-table tr { + height: 71px; } } @@ -969,3 +916,22 @@ } } } + +/** + * Play button with icon in dropdowns + */ +.ci-table .no-btn { + border: none; + background: none; + outline: none; + width: 100%; + text-align: left; + + .icon-play { + position: relative; + top: 2px; + margin-right: 5px; + height: 13px; + width: 12px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8031c4467a4..1a983d8c9ef 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -277,3 +277,42 @@ table.u2f-registrations { padding-left: 18px; } } + +.user-callout { + margin: 0 auto; + + .bordered-box { + border: 1px solid $border-color; + border-radius: $border-radius-default; + } + + .landing { + margin-top: $gl-padding; + margin-bottom: $gl-padding; + + .close { + margin-right: 20px; + } + + .dismiss-icon { + float: right; + cursor: pointer; + color: $cycle-analytics-dismiss-icon-color; + } + + .svg-container { + text-align: center; + + svg { + width: 136px; + height: 136px; + } + } + } + + @media(max-width: $screen-xs-max) { + .inner-content { + padding-left: 30px; + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8c0de314420..efa47be9a73 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -494,11 +494,11 @@ a.deploy-project-label { .project-stats { font-size: 0; text-align: center; - border-bottom: 1px solid $border-color; .nav { padding-top: 12px; padding-bottom: 12px; + border-bottom: 1px solid $border-color; } .nav > li { @@ -638,39 +638,22 @@ pre.light-well { margin: 0; } -.activity-filter-block { - .controls { - padding-bottom: 7px; - margin-top: 8px; - border-bottom: 1px solid $border-color; - } -} - .commits-search-form { .input-short { min-width: 200px; } } -.container-fluid.project-stats-container { - @media (max-width: $screen-xs-max) { - padding: 12px 0; - } -} - .project-last-commit { background-color: $gray-light; - padding: 12px $gl-padding; border: 1px solid $border-color; + border-radius: $border-radius-base; + padding: 12px; @media (min-width: $screen-sm-min) { margin-top: $gl-padding; } - @media (min-width: $screen-sm-min) { - border-radius: $border-radius-base; - } - .ci-status { margin-right: $gl-padding; } @@ -763,6 +746,8 @@ pre.light-well { } .protected-branches-list { + margin-bottom: 30px; + a { color: $gl-text-color; @@ -810,7 +795,8 @@ pre.light-well { } .project-refs-form .dropdown-menu, -.dropdown-menu-projects { +.dropdown-menu-projects, +.dropdown-menu-branches { width: 300px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 88ea92c5afb..543d2ece3df 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -182,7 +182,8 @@ input[type="checkbox"]:hover { display: flex; } - .search-field-holder { + .search-field-holder, + .project-filter-form { -webkit-flex: 1 0 auto; flex: 1 0 auto; position: relative; @@ -201,7 +202,8 @@ input[type="checkbox"]:hover { pointer-events: none; } - .search-text-input { + .search-text-input, + .project-filter-form-field { padding-left: $gl-padding + 15px; padding-right: $gl-padding + 15px; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index a28a87ed4f8..3889deee21a 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -24,3 +24,14 @@ .service-settings .control-label { padding-top: 0; } + +.token-token-container { + #impersonation-token-token { + width: 80%; + display: inline; + } + + .btn-clipboard { + margin-left: 5px; + } +} diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss new file mode 100644 index 00000000000..b97a29cd1a0 --- /dev/null +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -0,0 +1,12 @@ +.triggers-container { + .label-container { + display: inline-block; + margin-left: 10px; + } +} + +.trigger-actions { + .btn { + margin-left: 10px; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index af9ddb9ff80..5f0aede4f5e 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -170,7 +170,11 @@ @media (max-width: $screen-sm-max) { .todos-filters { .dropdown-menu-toggle { - width: 135px; + width: 130px; + } + + .dropdown-menu-toggle-sort { + width: auto; } } } @@ -200,10 +204,6 @@ } .todos-filters { - .row-content-block { - padding-bottom: 50px; - } - .dropdown-menu-toggle { width: 100%; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e4487dbcb87..fc4da4c495f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -139,18 +139,10 @@ .blob-commit-info { list-style: none; background: $gray-light; - padding: 6px 0; + padding: 16px 16px 16px 6px; border: 1px solid $border-color; border-bottom: none; margin: 0; - - .table-list-cell { - border-bottom: none; - } - - .commit-actions { - width: 260px; - } } #modal-remove-blob > .modal-dialog { width: 850px; } @@ -178,3 +170,29 @@ margin-left: $btn-side-margin; } } + +.repo-charts { + .sub-header { + margin: 20px 0; + } + + .sub-header-block.border-top { + margin-top: 20px; + padding: 0; + border-top: 1px solid $white-dark; + border-bottom: none; + } + + .commit-stats li { + font-size: 16px; + } + + .tree-ref-header { + margin-bottom: 20px; + + h4 { + margin: 0; + line-height: 36px; + } + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index fb6df1a06d2..1d0bd6e0b81 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -89,6 +89,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_api_key, :akismet_enabled, :container_registry_token_expire_delay, + :default_artifacts_expire_in, :default_branch_protection, :default_group_visibility, :default_project_visibility, @@ -143,6 +144,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :two_factor_grace_period, :user_default_external, :user_oauth_applications, + :unique_ips_limit_per_user, + :unique_ips_limit_time_window, + :unique_ips_limit_enabled, :version_check_enabled, :terminal_max_session_time, diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 62f62e99a97..9c9f420c1e0 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController include OauthApplications before_action :set_application, only: [:show, :edit, :update, :destroy] - before_action :load_scopes, only: [:new, :edit] + before_action :load_scopes, only: [:new, :create, :edit, :update] def index @applications = Doorkeeper::Application.where("owner_id IS NULL") diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 241c7be0ea1..caf4c138da8 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,5 +1,5 @@ class Admin::HealthCheckController < Admin::ApplicationController def show - @errors = HealthCheck::Utils.process_checks('standard') + @errors = HealthCheck::Utils.process_checks(['standard']) end end diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb new file mode 100644 index 00000000000..07c8bf714fc --- /dev/null +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -0,0 +1,53 @@ +class Admin::ImpersonationTokensController < Admin::ApplicationController + before_action :user + + def index + set_index_vars + end + + def create + @impersonation_token = finder.build(impersonation_token_params) + + if @impersonation_token.save + flash[:impersonation_token] = @impersonation_token.token + redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created." + else + set_index_vars + render :index + end + end + + def revoke + @impersonation_token = finder.find(params[:id]) + + if @impersonation_token.revoke! + flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!" + else + flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}." + end + + redirect_to admin_user_impersonation_tokens_path + end + + private + + def user + @user ||= User.find_by!(username: params[:user_id]) + end + + def finder(options = {}) + PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) + end + + def impersonation_token_params + params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: []) + end + + def set_index_vars + @scopes = Gitlab::Auth::API_SCOPES + + @impersonation_token ||= finder.build + @inactive_impersonation_tokens = finder(state: 'inactive').execute + @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at) + end +end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 39c8c6d8a0c..daecfc832bf 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -14,6 +14,15 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects }) + } + end + end end def show diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index 1330399a836..99039724521 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -3,7 +3,7 @@ class Admin::SystemInfoController < Admin::ApplicationController 'nobrowse', 'read-only', 'ro' - ] + ].freeze EXCLUDED_MOUNT_TYPES = [ 'autofs', @@ -27,7 +27,7 @@ class Admin::SystemInfoController < Admin::ApplicationController 'tmpfs', 'tracefs', 'vfat' - ] + ].freeze def show @cpus = Vmstat.cpu rescue nil diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 7ffde71c3b1..24504685e48 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -29,11 +29,7 @@ class Admin::UsersController < Admin::ApplicationController end def impersonate - if user.blocked? - flash[:alert] = "You cannot impersonate a blocked user" - - redirect_to admin_user_path(user) - else + if can?(user, :log_in) session[:impersonator_id] = current_user.id warden.set_user(user, scope: :user) @@ -43,6 +39,17 @@ class Admin::UsersController < Admin::ApplicationController flash[:alert] = "You are now impersonating #{user.username}" redirect_to root_path + else + flash[:alert] = + if user.blocked? + "You cannot impersonate a blocked user" + elsif user.internal? + "You cannot impersonate an internal user" + else + "You cannot impersonate a user who cannot log in" + end + + redirect_to admin_user_path(user) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5e7af3bff0d..b7ce081a5cd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base render_403 end + rescue_from Gitlab::Auth::TooManyIps do |e| + head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window + end + def redirect_back_or_default(default: root_path, options: {}) redirect_to request.referer.present? ? :back : default, options end @@ -63,7 +67,7 @@ class ApplicationController < ActionController::Base token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) - if user + if user && can?(user, :log_in) # Notice we are passing store false, so the user is not # actually stored in the session and a token is needed # for every request. If you want the token to work as a @@ -72,14 +76,6 @@ class ApplicationController < ActionController::Base end end - def authenticate_user!(*args) - if redirect_to_home_page_url? - return redirect_to current_application_settings.home_page_url - end - - super(*args) - end - def log_exception(exception) application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace application_trace.map!{ |t| " #{t}\n" } @@ -94,7 +90,7 @@ class ApplicationController < ActionController::Base current_application_settings.after_sign_out_path.presence || new_user_session_path end - def can?(object, action, subject) + def can?(object, action, subject = :global) Ability.allowed?(object, action, subject) end @@ -130,10 +126,6 @@ class ApplicationController < ActionController::Base headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' - # Enabling HSTS for non-standard ports would send clients to the wrong port - if Gitlab.config.gitlab.https && Gitlab.config.gitlab.port == 443 - headers['Strict-Transport-Security'] = 'max-age=31536000' - end end def validate_user_service_ticket! @@ -181,7 +173,7 @@ class ApplicationController < ActionController::Base end def gitlab_ldap_access(&block) - Gitlab::LDAP::Access.open { |access| block.call(access) } + Gitlab::LDAP::Access.open { |access| yield(access) } end # JSON for infinite scroll via Pager object @@ -287,19 +279,6 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end - def redirect_to_home_page_url? - # If user is not signed-in and tries to access root_path - redirect him to landing page - # Don't redirect to the default URL to prevent endless redirections - return false unless current_application_settings.home_page_url.present? - - home_page_url = current_application_settings.home_page_url.chomp('/') - root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] - - return false if root_urls.include?(home_page_url) - - current_user.nil? && root_path == request.path - end - # U2F (universal 2nd factor) devices need a unique identifier for the application # to perform authentication. # https://developers.yubico.com/U2F/App_ID.html diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index d7a45bacd35..b79ca034c5b 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user].present? && current_user - @users = @users.where.not(id: current_user.id) - @users = [current_user, *@users] + @users = [current_user, *@users].uniq end if params[:author_id].present? diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb deleted file mode 100644 index ff297d6ff13..00000000000 --- a/app/controllers/ci/projects_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Ci - class ProjectsController < ::ApplicationController - before_action :project - before_action :no_cache, only: [:badge] - before_action :authorize_read_project!, except: [:badge, :index] - skip_before_action :authenticate_user!, only: [:badge] - protect_from_forgery - - def index - redirect_to root_path - end - - def show - # Temporary compatibility with CI badges pointing to CI project page - redirect_to namespace_project_path(project.namespace, project) - end - - # Project status badge - # Image with build status for sha or ref - # - # This action in DEPRECATED, this is here only for backwards compatibility - # with projects migrated from GitLab CI. - # - def badge - return render_404 unless @project - - image = Ci::ImageForBuildService.new.execute(@project, params) - send_file image.path, filename: image.name, disposition: 'inline', type: "image/svg+xml" - end - - protected - - def project - @project ||= Project.find_by(ci_id: params[:id].to_i) - end - - def no_cache - response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" - end - - def authorize_read_project! - return access_denied! unless can?(current_user, :read_project, project) - end - end -end diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index 4c497711fc0..ea441b1736b 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -23,7 +23,7 @@ module AuthenticatesWithTwoFactor # # Returns nil def prompt_for_two_factor(user) - return locked_user_redirect(user) if user.access_locked? + return locked_user_redirect(user) unless user.can?(:log_in) session[:otp_user_id] = user.id setup_u2f_authentication(user) @@ -37,10 +37,9 @@ module AuthenticatesWithTwoFactor def authenticate_with_two_factor user = self.resource = find_user + return locked_user_redirect(user) unless user.can?(:log_in) - if user.access_locked? - locked_user_redirect(user) - elsif user_params[:otp_attempt].present? && session[:otp_user_id] + if user_params[:otp_attempt].present? && session[:otp_user_id] authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] authenticate_with_two_factor_via_u2f(user) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 88d180fcc2e..9ac8197e45a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,10 +4,9 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - start_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( start_project: @mr_target_project, - start_branch: start_branch, + start_branch: @mr_target_branch, target_branch: @mr_source_branch ) @@ -17,12 +16,16 @@ module CreatesCommit if result[:status] == :success update_flash_notice(success_notice) + success_path = final_success_path(success_path) + respond_to do |format| - format.html { redirect_to final_success_path(success_path) } - format.json { render json: { message: "success", filePath: final_success_path(success_path) } } + format.html { redirect_to success_path } + format.json { render json: { message: "success", filePath: success_path } } end else flash[:alert] = result[:message] + failure_path = failure_path.call if failure_path.respond_to?(:call) + respond_to do |format| format.html do if failure_view @@ -58,9 +61,13 @@ module CreatesCommit end def final_success_path(success_path) - return success_path unless create_merge_request? + if create_merge_request? + merge_request_exists? ? existing_merge_request_path : new_merge_request_path + else + success_path = success_path.call if success_path.respond_to?(:call) - merge_request_exists? ? existing_merge_request_path : new_merge_request_path + success_path + end end def new_merge_request_path @@ -92,46 +99,26 @@ module CreatesCommit end def create_merge_request? - # XXX: Even if the field is set, if we're checking the same branch + # Even if the field is set, if we're checking the same branch # as the target branch in the same project, # we don't want to create a merge request. params[:create_merge_request].present? && - (different_project? || @ref != @target_branch) + (different_project? || @mr_target_branch != @mr_source_branch) end - # TODO: We should really clean this up def set_commit_variables if can?(current_user, :push_code, @project) - # Edit file in this project @mr_source_project = @project + @target_branch ||= @ref else - # Merge request from fork to this project @mr_source_project = current_user.fork_of(@project) + @target_branch ||= @mr_source_project.repository.next_branch('patch') end # Merge request to this project @mr_target_project = @project - @mr_target_branch = @ref || @target_branch - - @mr_source_branch = guess_mr_source_branch - end - - def initial_commit? - @mr_target_branch.nil? || - !@mr_target_project.repository.branch_exists?(@mr_target_branch) - end + @mr_target_branch ||= @ref || @target_branch - def guess_mr_source_branch - # XXX: Happens when viewing a commit without a branch. In this case, - # @target_branch would be the default branch for @mr_source_project, - # however we want a generated new branch here. Thus we can't use - # @target_branch, but should pass nil to indicate that we want a new - # branch instead of @target_branch. - return if - create_merge_request? && - # XXX: Don't understand why rubocop prefers this indention - @mr_source_project.repository.branch_exists?(@target_branch) - - @target_branch + @mr_source_branch = @target_branch end end diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb index 586f97c5eb4..6014112256a 100644 --- a/app/controllers/concerns/filter_projects.rb +++ b/app/controllers/concerns/filter_projects.rb @@ -8,7 +8,7 @@ module FilterProjects extend ActiveSupport::Concern def filter_projects(projects) - projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? + projects = projects.search(params[:name]) if params[:name].present? projects = projects.non_archived if params[:archived].blank? projects = projects.personal(current_user) if params[:personal].present? && current_user diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0821974aa93..3ccf2a9ce33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -26,6 +26,23 @@ module IssuableActions private + def render_conflict_response + respond_to do |format| + format.html do + @conflict = true + render :edit + end + + format.json do + render json: { + errors: [ + "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs." + ] + }, status: 409 + end + end + end + def labels @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a6e158ebae6..85ae4985e58 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -9,24 +9,32 @@ module IssuableCollections private - def issuable_meta_data(issuable_collection) + def issuable_meta_data(issuable_collection, collection_type) # map has to be used here since using pluck or select will # throw an error when ordering issuables by priority which inserts # a new order into the collection. # We cannot use reorder to not mess up the paginated collection. - issuable_ids = issuable_collection.map(&:id) - issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) + issuable_ids = issuable_collection.map(&:id) + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) + issuable_merge_requests_count = + if collection_type == 'Issue' + MergeRequestsClosingIssues.count_for_collection(issuable_ids) + else + [] + end issuable_ids.each_with_object({}) do |id, issuable_meta| downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? } - upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } - notes = issuable_note_count.find { |notes| notes.noteable_id == id } + upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? } + notes = issuable_note_count.find { |notes| notes.noteable_id == id } + merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id } issuable_meta[id] = Issuable::IssuableMeta.new( upvotes.try(:count).to_i, downvotes.try(:count).to_i, - notes.try(:count).to_i + notes.try(:count).to_i, + merge_requests.try(:last).to_i ) end end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index fb5edb34370..b17c138d5c7 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -10,7 +10,7 @@ module IssuesAction .page(params[:page]) @collection_type = "Issue" - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6229759dcf1..d3c8e4888bc 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -9,7 +9,7 @@ module MergeRequestsAction .page(params[:page]) @collection_type = "MergeRequest" - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) end private diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb new file mode 100644 index 00000000000..0854c73a02f --- /dev/null +++ b/app/controllers/concerns/repository_settings_redirect.rb @@ -0,0 +1,7 @@ +module RepositorySettingsRedirect + extend ActiveSupport::Concern + + def redirect_to_repository_settings(project) + redirect_to namespace_project_settings_repository_path(project.namespace, project) + end +end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index d7f5a4e4682..a8c0937569c 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -33,10 +33,10 @@ module ServiceParams :issues_url, :jira_issue_transition_id, :merge_requests_events, + :mock_service_url, :namespace, :new_issue_url, :notify, - :notify_only_broken_builds, :notify_only_broken_pipelines, :password, :priority, @@ -59,10 +59,10 @@ module ServiceParams :user_key, :username, :webhook - ] + ].freeze # Parameters to ignore if no value is specified - FILTER_BLANK_PARAMS = [:password] + FILTER_BLANK_PARAMS = [:password].freeze def service_params dynamic_params = @service.event_channel_names + @service.event_names diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index da225d8f1c7..d0a692070d9 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -27,7 +27,7 @@ module SpammableActions render :verify else - fallback.call + yield end end diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 0b7cf8167f0..d03265e9f20 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,5 +1,17 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index - @group_members = current_user.group_members.includes(source: :route).page(params[:page]) + @group_members = current_user.group_members.includes(source: :route).joins(:group) + @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? + @group_members = @group_members.merge(Group.sort(@sort = params[:sort])) + @group_members = @group_members.page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) + } + end + end end end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 7f506db583f..df528d10f6e 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def index respond_to do |format| format.html do + @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 325ae565537..be00d765f73 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -42,7 +42,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController private def load_projects(base_scope) - projects = base_scope.sorted_by_activity.includes(:namespace) + projects = base_scope.sorted_by_activity.includes(:route, namespace: :route) filter_projects(projects) end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 5848ca62777..096de8032ae 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy_all - TodoService.new.mark_todos_as_done(@todos, current_user) + updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user) respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.js { head :ok } - format.json { render json: todos_counts } + format.json { render json: todos_counts.merge(updated_ids: updated_ids) } end end @@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController render json: todos_counts end + def bulk_restore + TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user) + + render json: todos_counts + end + # Used in TodosHelper also def self.todos_count_format(count) count >= 100 ? '99+' : count @@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController private def find_todos - @todos ||= TodosFinder.new(current_user, params).execute + @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute end def todos_counts diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb deleted file mode 100644 index 1bec5a7d27f..00000000000 --- a/app/controllers/emojis_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class EmojisController < ApplicationController - layout false - - def index - end -end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index a962f9a0937..68228c095da 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,8 +1,17 @@ class Explore::GroupsController < Explore::ApplicationController def index @groups = GroupsFinder.new.execute(current_user) - @groups = @groups.search(params[:search]) if params[:search].present? + @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.page(params[:page]) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups }) + } + end + end end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 26e17a7553e..6167f9bd335 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -2,7 +2,7 @@ class Explore::ProjectsController < Explore::ApplicationController include FilterProjects def index - @projects = ProjectsFinder.new.execute(current_user) + @projects = load_projects @tags = @projects.tags_on(:tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @@ -21,7 +21,8 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = filter_projects(Project.trending) + @projects = load_projects(Project.trending) + @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) @@ -36,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def starred - @projects = ProjectsFinder.new.execute(current_user) + @projects = load_projects @projects = filter_projects(@projects) @projects = @projects.reorder('star_count DESC') @projects = @projects.page(params[:page]) @@ -50,4 +51,11 @@ class Explore::ProjectsController < Explore::ApplicationController end end end + + protected + + def load_projects(base_scope = nil) + base_scope ||= ProjectsFinder.new.execute(current_user) + base_scope.includes(:route, namespace: :route) + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 0d872c86c8a..43102596201 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do + @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7ed54479599..05f9ee1ee90 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController @group = Groups::CreateService.new(current_user, group_params).execute if @group.persisted? - redirect_to @group, notice: "Group '#{@group.name}' was successfully created." + notice = if @group.chat_team.present? + "Group '#{@group.name}' and its Mattermost team were successfully created." + else + "Group '#{@group.name}' was successfully created." + end + + redirect_to @group, notice: notice else render action: "new" end @@ -108,11 +114,11 @@ class GroupsController < Groups::ApplicationController @projects = @projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]) if params[:name].blank? end def authorize_create_group! - unless can?(current_user, :create_group, nil) + unless can?(current_user, :create_group) return render_404 end end @@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level, - :parent_id + :parent_id, + :create_chat_team, + :chat_team_name ] end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index c2e4d62b50b..3109439b2ff 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -5,7 +5,7 @@ class JwtController < ApplicationController SERVICES = { Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, - } + }.freeze def auth service = SERVICES[params[:service]] @@ -39,7 +39,8 @@ class JwtController < ApplicationController message: "HTTP Basic: Access denied\n" \ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ "You can generate one at #{profile_personal_access_tokens_url}" } - ] }, status: 401 + ] + }, status: 401 end def render_unauthorized @@ -47,7 +48,8 @@ class JwtController < ApplicationController errors: [ { code: 'UNAUTHORIZED', message: 'HTTP Basic: Access denied' } - ] }, status: 401 + ] + }, status: 401 end def auth_params diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index c721dca58d9..05190103767 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -1,8 +1,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController - before_action :authenticate_resource_owner! - layout 'profile' + # Overriden from Doorkeeper::AuthorizationsController to + # include the call to session.delete def new if pre_auth.authorizable? if skip_authorization? || matching_token? @@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController render "doorkeeper/authorizations/error" end end - - # TODO: Handle raise invalid authorization - def create - redirect_or_render authorization.authorize - end - - def destroy - redirect_or_render authorization.deny - end - - private - - def matching_token? - Doorkeeper::AccessToken.matching_token_for(pre_auth.client, - current_resource_owner.id, - pre_auth.scopes) - end - - def redirect_or_render(auth) - if auth.redirectable? - redirect_to auth.redirect_uri - else - render json: auth.body, status: auth.status - end - end - - def pre_auth - @pre_auth ||= - Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration, - server.client_via_uid, - params) - end - - def authorization - @authorization ||= strategy.request - end - - def strategy - @strategy ||= server.authorization_request(pre_auth.response_type) - end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index c8663a3c38e..e4452f46056 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -10,11 +10,6 @@ class Profiles::KeysController < Profiles::ApplicationController @key = current_user.keys.find(params[:id]) end - # Back-compat: We need to support this URL since git-annex webapp points to it - def new - redirect_to profile_keys_path - end - def create @key = current_user.keys.new(key_params) diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index a271e2dfc4b..b8b71d295f6 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def user_params - params.require(:user).permit(:notification_email, :notified_of_own_activity) + params.require(:user).permit(:notification_email) end end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 6e007f17913..0abe7ea3c9b 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def create - @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params) + @personal_access_token = finder.build(personal_access_token_params) if @personal_access_token.save flash[:personal_access_token] = @personal_access_token.token @@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def revoke - @personal_access_token = current_user.personal_access_tokens.find(params[:id]) + @personal_access_token = finder.find(params[:id]) if @personal_access_token.revoke! flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!" @@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController private + def finder(options = {}) + PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options)) + end + def personal_access_token_params params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end def set_index_vars - @personal_access_token ||= current_user.personal_access_tokens.build - @scopes = Gitlab::Auth::SCOPES - @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) - @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive + @scopes = Gitlab::Auth::API_SCOPES + + @personal_access_token = finder.build + @inactive_personal_access_tokens = finder(state: 'inactive').execute + @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) end end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 18044ca78e2..26e7e93533e 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -80,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def build_qr_code uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host) - RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) + RQRCode.render_qrcode(uri, :svg, level: :m, unit: 3) end def account_string diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f0c71725ea8..987b95e89b9 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -47,11 +47,14 @@ class ProfilesController < Profiles::ApplicationController end def update_username - @user.update_attributes(username: user_params[:username]) - - respond_to do |format| - format.js + if @user.update_attributes(username: user_params[:username]) + options = { notice: "Username successfully changed" } + else + message = @user.errors.full_messages.uniq.join('. ') + options = { alert: "Username change failed - #{message}" } end + + redirect_back_or_default(default: { action: 'show' }, options: options) end private diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index d9dfa534669..ffb54390965 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -1,9 +1,5 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController - before_action :load_autocomplete_service, except: [:emojis, :members] - - def emojis - render json: Gitlab::AwardEmoji.urls - end + before_action :load_autocomplete_service, except: [:members] def members render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 863a766a255..6461eeac11c 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - + return render_404 unless @blob + environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last + @blame_groups = Gitlab::Blame.new(@blob, @commit).groups end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 39ba815cfca..52fc67d162c 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -5,7 +5,7 @@ class Projects::BlobController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path - class InvalidPathError < StandardError; end + InvalidPathError = Class.new(StandardError) before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! @@ -23,8 +23,10 @@ class Projects::BlobController < Projects::ApplicationController end def create + update_ref + create_commit(Files::CreateService, success_notice: "The file has been successfully created.", - success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)), + success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }, failure_view: :new, failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end @@ -40,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - create_commit(Files::UpdateService, success_path: after_edit_path, + create_commit(Files::UpdateService, success_path: -> { after_edit_path }, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -62,7 +64,7 @@ class Projects::BlobController < Projects::ApplicationController def destroy create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.", - success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), + success_path: -> { namespace_project_tree_path(@project.namespace, @project, @target_branch) }, failure_view: :show, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end @@ -87,6 +89,11 @@ class Projects::BlobController < Projects::ApplicationController private + def update_ref + branch_exists = @repository.find_branch(@target_branch) + @ref = @target_branch if branch_exists + end + def blob @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path)) diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 61fef4dc133..28c9646910d 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -8,6 +8,7 @@ module Projects def index issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = issues.page(params[:page]).per(params[:per] || 20) + make_sure_position_is_set(issues) render json: { issues: serialize_as_json(issues), @@ -38,6 +39,12 @@ module Projects private + def make_sure_position_is_set(issues) + issues.each do |issue| + issue.move_to_end && issue.save unless issue.relative_position + end + end + def issue @issue ||= IssuesFinder.new(current_user, project_id: project.id) @@ -63,7 +70,7 @@ module Projects end def move_params - params.permit(:board_id, :id, :from_list_id, :to_list_id) + params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid) end def issue_params @@ -73,7 +80,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:id, :iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date, :relative_position], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 89d84809e3a..840405f38cb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,25 +1,29 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper include SortingHelper + # Authorize - before_action :require_non_empty_project + before_action :require_non_empty_project, except: :create before_action :authorize_download_code! before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged] def index @sort = params[:sort].presence || sort_value_name @branches = BranchesFinder.new(@repository, params).execute - @branches = Kaminari.paginate_array(@branches).page(params[:page]) - - @max_commits = @branches.reduce(0) do |memo, branch| - diverging_commit_counts = repository.diverging_commit_counts(branch) - [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max - end respond_to do |format| - format.html + format.html do + paginate_branches + @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + + @max_commits = @branches.reduce(0) do |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end + end format.json do - render json: @repository.branch_names + paginate_branches unless params[:show_all] + render json: @branches.map(&:name) end end end @@ -32,6 +36,8 @@ class Projects::BranchesController < Projects::ApplicationController branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = Addressable::URI.unescape(branch_name) + redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present? + result = CreateBranchService.new(project, current_user). execute(branch_name, ref) @@ -42,8 +48,15 @@ class Projects::BranchesController < Projects::ApplicationController if result[:status] == :success @branch = result[:branch] - redirect_to namespace_project_tree_path(@project.namespace, @project, - @branch.name) + + if redirect_to_autodeploy + redirect_to( + url_to_autodeploy_setup(project, branch_name), + notice: view_context.autodeploy_flash_notice(branch_name)) + else + redirect_to namespace_project_tree_path(@project.namespace, @project, + @branch.name) + end else @error = result[:message] render action: 'new' @@ -76,7 +89,23 @@ class Projects::BranchesController < Projects::ApplicationController ref_escaped = sanitize(strip_tags(params[:ref])) Addressable::URI.unescape(ref_escaped) else - @project.default_branch + @project.default_branch || 'master' end end + + def paginate_branches + @branches = Kaminari.paginate_array(@branches).page(params[:page]) + end + + def url_to_autodeploy_setup(project, branch_name) + namespace_project_new_blob_path( + project.namespace, + project, + branch_name, + file_name: '.gitlab-ci.yml', + commit_message: 'Set up auto deploy', + target_branch: branch_name, + context: 'autodeploy' + ) + end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index e10d7992db7..cc67f688d51 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -51,23 +51,35 @@ class Projects::CommitController < Projects::ApplicationController def revert assign_change_commit_vars - return render_404 if @target_branch.blank? + return render_404 if @start_branch.blank? + + @target_branch = create_new_branch? ? @commit.revert_branch_name : @start_branch + + @mr_target_branch = @start_branch create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", - success_path: successful_change_path, failure_path: failed_change_path) + success_path: -> { successful_change_path }, failure_path: failed_change_path) end def cherry_pick assign_change_commit_vars - return render_404 if @target_branch.blank? + return render_404 if @start_branch.blank? + + @target_branch = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch + + @mr_target_branch = @start_branch create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", - success_path: successful_change_path, failure_path: failed_change_path) + success_path: -> { successful_change_path }, failure_path: failed_change_path) end private + def create_new_branch? + params[:create_merge_request].present? || !can?(current_user, :push_code, @project) + end + def successful_change_path referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end @@ -78,7 +90,7 @@ class Projects::CommitController < Projects::ApplicationController def referenced_merge_request_url if merge_request = @commit.merged_merge_request(current_user) - namespace_project_merge_request_url(@project.namespace, @project, merge_request) + namespace_project_merge_request_url(merge_request.target_project.namespace, merge_request.target_project, merge_request) end end @@ -94,7 +106,7 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count - + @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last end @@ -118,11 +130,7 @@ class Projects::CommitController < Projects::ApplicationController end def assign_change_commit_vars - @commit = project.commit(params[:id]) - @target_branch = params[:target_branch] - @commit_params = { - commit: @commit, - create_merge_request: params[:create_merge_request].present? || different_project? - } + @start_branch = params[:start_branch] + @commit_params = { commit: @commit } end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index b094491e006..1502b734f37 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -1,4 +1,5 @@ class Projects::DeployKeysController < Projects::ApplicationController + include RepositorySettingsRedirect respond_to :html # Authorize @@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController layout "project_settings" def index - @key = DeployKey.new - set_index_vars + redirect_to_repository_settings(@project) end def new - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) + redirect_to_repository_settings(@project) end def create @key = DeployKey.new(deploy_key_params.merge(user: current_user)) - set_index_vars - if @key.valid? && @project.deploy_keys << @key - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) - else - render "index" + unless @key.valid? && @project.deploy_keys << @key + flash[:alert] = @key.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end def enable Projects::EnableDeployKeyService.new(@project, current_user, params).execute - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) + redirect_to_repository_settings(@project) end def disable @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy - redirect_back_or_default(default: { action: 'index' }) + redirect_to_repository_settings(@project) end protected - def set_index_vars - @enabled_keys ||= @project.deploy_keys - - @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys - @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys - @available_public_keys ||= DeployKey.are_public - @enabled_keys - - # Public keys that are already used by another accessible project are already - # in @available_project_keys. - @available_public_keys -= @available_project_keys - end - def deploy_key_params params.require(:deploy_key).permit(:key, :title, :can_push) end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index fed75396d6e..fa37963dfd4 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] - before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :verify_api_request!, only: :terminal_websocket_authorize def index @@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + def metrics + # Currently, this acts as a hint to load the metrics details into the cache + # if they aren't there already + @metrics = environment.metrics || {} + + respond_to do |format| + format.html + format.json do + render json: @metrics, status: @metrics.any? ? :ok : :no_content + end + end + end + private def verify_api_request! diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 216c158e41e..9a1bf037a95 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -76,11 +76,12 @@ class Projects::GitHttpClientController < Projects::ApplicationController return @project if defined?(@project) project_id, _ = project_id_with_suffix - if project_id.blank? - @project = nil - else - @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") - end + @project = + if project_id.blank? + nil + else + Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") + end end # This method returns two values so that we can parse diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 923e7340e69..43fc0c39801 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -17,6 +17,25 @@ class Projects::GraphsController < Projects::ApplicationController end def commits + redirect_to action: 'charts' + end + + def languages + redirect_to action: 'charts' + end + + def charts + get_commits + get_languages + end + + def ci + redirect_to charts_namespace_project_pipelines_path(@project.namespace, @project) + end + + private + + def get_commits @commits = @project.repository.commits(@ref, limit: 2000, skip_merges: true) @commits_graph = Gitlab::Graphs::Commits.new(@commits) @commits_per_week_days = @commits_graph.commits_per_week_days @@ -24,15 +43,7 @@ class Projects::GraphsController < Projects::ApplicationController @commits_per_month = @commits_graph.commits_per_month end - def ci - @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(project) - @charts[:month] = Ci::Charts::MonthChart.new(project) - @charts[:year] = Ci::Charts::YearChart.new(project) - @charts[:build_times] = Ci::Charts::BuildTime.new(project) - end - - def languages + def get_languages @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages total = @languages.map(&:last).sum @@ -52,8 +63,6 @@ class Projects::GraphsController < Projects::ApplicationController end end - private - def fetch_graph @commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true) @log = [] diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6ef36771ac1..cdb5b4173d3 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableCollections include SpammableActions + prepend_before_action :authenticate_user!, only: [:new] + before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -26,7 +28,7 @@ class Projects::IssuesController < Projects::ApplicationController @collection_type = "Issue" @issues = issues_collection @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 return redirect_to url_for(params.merge(page: @issues.total_pages)) @@ -64,8 +66,15 @@ class Projects::IssuesController < Projects::ApplicationController params[:issue] ||= ActionController::Parameters.new( assignee_id: "" ) - build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) - @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute + build_params = issue_params.merge( + merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], + discussion_to_resolve: params[:discussion_to_resolve] + ) + service = Issues::BuildService.new(project, current_user, build_params) + + @issue = @noteable = service.execute + @merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of + @discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve] respond_with(@issue) end @@ -94,11 +103,21 @@ class Projects::IssuesController < Projects::ApplicationController end def create - create_params = issue_params - .merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) - .merge(spammable_params) + create_params = issue_params.merge(spammable_params).merge( + merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], + discussion_to_resolve: params[:discussion_to_resolve] + ) - @issue = Issues::CreateService.new(project, current_user, create_params).execute + service = Issues::CreateService.new(project, current_user, create_params) + @issue = service.execute + + if service.discussions_to_resolve.count(&:resolved?) > 0 + flash[:notice] = if service.discussion_to_resolve_id + "Resolved 1 discussion." + else + "Resolved all discussions." + end + end respond_to do |format| format.html do @@ -129,13 +148,12 @@ class Projects::IssuesController < Projects::ApplicationController end format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) end end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def referenced_merge_requests @@ -186,14 +204,6 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :awardable, :issue alias_method :spammable, :issue - def merge_request_for_resolving_discussions - return unless merge_request_iid = params[:merge_request_for_resolving_discussions] - - @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id). - execute. - find_by(iid: merge_request_iid) - end - def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 75971faa93e..677a8a1a73a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,11 +10,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, - :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] - before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] @@ -39,7 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@merge_requests) + @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) @@ -245,9 +245,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do define_pipelines_vars - render json: PipelineSerializer + render json: { + pipelines: PipelineSerializer .new(project: @project, user: @current_user) .represent(@pipelines) + } end end end @@ -296,22 +298,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) - if @merge_request.valid? - respond_to do |format| - format.html do - redirect_to([@merge_request.target_project.namespace.becomes(Namespace), - @merge_request.target_project, @merge_request]) - end - format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + respond_to do |format| + format.html do + if @merge_request.valid? + redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) + else + render :edit end end - else - render "edit" + + format.json do + render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + end end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def remove_wip @@ -323,12 +324,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge_check @merge_request.check_if_can_be_merged + @pipelines = @merge_request.all_pipelines render partial: "projects/merge_requests/widget/show.html.haml", layout: false end - def cancel_merge_when_build_succeeds - unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + def cancel_merge_when_pipeline_succeeds + unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) return access_denied! end @@ -340,9 +342,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - # Disable the CI check if merge_when_build_succeeds is enabled since we have + # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have # to wait until CI completes to know - unless @merge_request.mergeable?(skip_ci_check: merge_when_build_succeeds_active?) + unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?) @status = :failed return end @@ -354,7 +356,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds].present? + if params[:merge_when_pipeline_succeeds].present? unless @merge_request.head_pipeline @status = :failed return @@ -365,7 +367,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController .new(@project, current_user, merge_params) .execute(@merge_request) - @status = :merge_when_build_succeeds + @status = :merge_when_pipeline_succeeds elsif @merge_request.head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time @@ -381,14 +383,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_widget_refresh - if merge_request.merge_when_build_succeeds - @status = :merge_when_build_succeeds - else - # Only MRs that can be merged end in this action - # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up - # in last case it does not have any special status. Possible error is handled inside widget js function - @status = :success - end + @status = + if merge_request.merge_when_pipeline_succeeds + :merge_when_pipeline_succeeds + else + # Only MRs that can be merged end in this action + # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up + # in last case it does not have any special status. Possible error is handled inside widget js function + :success + end render 'merge' end @@ -444,6 +447,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_status pipeline = @merge_request.head_pipeline + @pipelines = @merge_request.all_pipelines if pipeline status = pipeline.status @@ -462,7 +466,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, coverage: coverage, - pipeline: pipeline.try(:id) + pipeline: pipeline.try(:id), + has_ci: @merge_request.has_ci? } render json: response @@ -672,8 +677,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.ensure_ref_fetched end - def merge_when_build_succeeds_active? - params[:merge_when_build_succeeds].present? && + def merge_when_pipeline_succeeds_active? + params[:merge_when_pipeline_succeeds].present? && @merge_request.head_pipeline && @merge_request.head_pipeline.active? end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index b033f7b5ea9..d00177e7612 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -148,17 +148,10 @@ class Projects::NotesController < Projects::ApplicationController def note_json(note) attrs = { - award: false, id: note.id } - if note.is_a?(AwardEmoji) - attrs.merge!( - valid: note.valid?, - award: true, - name: note.name - ) - elsif note.persisted? + if note.persisted? Banzai::NoteRenderer.render([note], @project, current_user) attrs.merge!( @@ -198,7 +191,7 @@ class Projects::NotesController < Projects::ApplicationController ) end - attrs[:commands_changes] = note.commands_changes unless attrs[:award] + attrs[:commands_changes] = note.commands_changes attrs end @@ -218,6 +211,11 @@ class Projects::NotesController < Projects::ApplicationController end def find_current_user_notes - @notes = NotesFinder.new(project, current_user, params).execute.inc_author + @notes = NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at)) + .execute.inc_author + end + + def last_fetched_at + request.headers['X-Last-Fetched-At'] end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 84451257b98..718d9e86bea 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,9 +1,10 @@ class Projects::PipelinesController < Projects::ApplicationController - before_action :pipeline, except: [:index, :new, :create] + before_action :pipeline, except: [:index, :new, :create, :charts] before_action :commit, only: [:show, :builds] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] + before_action :builds_enabled, only: :charts def index @scope = params[:scope] @@ -13,9 +14,15 @@ class Projects::PipelinesController < Projects::ApplicationController .page(params[:page]) .per(30) - @running_or_pending_count = PipelinesFinder + @running_count = PipelinesFinder .new(project).execute(scope: 'running').count + @pending_count = PipelinesFinder + .new(project).execute(scope: 'pending').count + + @finished_count = PipelinesFinder + .new(project).execute(scope: 'finished').count + @pipelines_count = PipelinesFinder .new(project).execute.count @@ -29,7 +36,9 @@ class Projects::PipelinesController < Projects::ApplicationController .represent(@pipelines), count: { all: @pipelines_count, - running_or_pending: @running_or_pending_count + running: @running_count, + pending: @pending_count, + finished: @finished_count, } } end @@ -84,6 +93,14 @@ class Projects::PipelinesController < Projects::ApplicationController redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end + def charts + @charts = {} + @charts[:week] = Ci::Charts::WeekChart.new(project) + @charts[:month] = Ci::Charts::MonthChart.new(project) + @charts[:year] = Ci::Charts::YearChart.new(project) + @charts[:build_times] = Ci::Charts::BuildTime.new(project) + end + private def create_params diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 2f422d352ed..a8cb07eb67a 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,26 +1,22 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController + include RepositorySettingsRedirect # Authorize before_action :require_non_empty_project before_action :authorize_admin_project! before_action :load_protected_branch, only: [:show, :update, :destroy] - before_action :load_protected_branches, only: [:index] layout "project_settings" def index - @protected_branch = @project.protected_branches.new - load_gon_index + redirect_to_repository_settings(@project) end def create @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute - if @protected_branch.persisted? - redirect_to namespace_project_protected_branches_path(@project.namespace, @project) - else - load_protected_branches - load_gon_index - render :index + unless @protected_branch.persisted? + flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end def show @@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController @protected_branch.destroy respond_to do |format| - format.html { redirect_to namespace_project_protected_branches_path } + format.html { redirect_to_repository_settings(@project) } format.js { head :ok } end end @@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController merge_access_levels_attributes: [:access_level, :id], push_access_levels_attributes: [:access_level, :id]) end - - def load_protected_branches - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - end - - def access_levels_options - { - push_access_levels: { - "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }, - }, - merge_access_levels: { - "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } } - } - } - end - - def load_gon_index - params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } } - gon.push(params.merge(access_levels_options)) - end end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 10d24da16d7..c55b37ae0dd 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController return if cached_blob? - if @blob.lfs_pointer? + if @blob.lfs_pointer? && project.lfs_enabled? send_lfs_object else send_git_blob @repository, @blob diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 17cb1d5be24..f9d798d0455 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController end def update - if @service.update_attributes(service_params[:service]) + @service.assign_attributes(service_params[:service]) + if @service.save(context: :manual_change) redirect_to( edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), notice: 'Successfully updated.' diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb index 5735e281f66..cbfa2afa959 100644 --- a/app/controllers/projects/settings/members_controller.rb +++ b/app/controllers/projects/settings/members_controller.rb @@ -7,47 +7,18 @@ module Projects @sort = params[:sort].presence || sort_value_name @group_links = @project.project_group_links - @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - - group = @project.group - - # group links - @group_links = @project.project_group_links.all - @skip_groups = @group_links.pluck(:group_id) @skip_groups << @project.namespace_id unless @project.personal? - if group - # We need `.where.not(user_id: nil)` here otherwise when a group has an - # invitee, it would make the following query return 0 rows since a NULL - # user_id would be present in the subquery - # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values - group_members = MembersFinder.new(@project_members, group).execute(current_user) - end + @project_members = MembersFinder.new(@project, current_user).execute if params[:search].present? - user_ids = @project.users.search(params[:search]).select(:id) - @project_members = @project_members.where(user_id: user_ids) - - if group_members - user_ids = group.users.search(params[:search]).select(:id) - group_members = group_members.where(user_id: user_ids) - end - - @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + @project_members = @project_members.joins(:user).merge(User.search(params[:search])) + @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] - wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members - - @project_members = Member. - where(wheres.join(' OR ')). - sort(@sort). - page(params[:page]) - + @project_members = @project_members.sort(@sort).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) - @project_member = @project.project_members.new end end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb new file mode 100644 index 00000000000..b6ce4abca45 --- /dev/null +++ b/app/controllers/projects/settings/repository_controller.rb @@ -0,0 +1,50 @@ +module Projects + module Settings + class RepositoryController < Projects::ApplicationController + before_action :authorize_admin_project! + + def show + @deploy_keys = DeployKeysPresenter + .new(@project, current_user: current_user) + + define_protected_branches + end + + private + + def define_protected_branches + load_protected_branches + @protected_branch = @project.protected_branches.new + load_gon_index + end + + def load_protected_branches + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + end + + def access_levels_options + { + push_access_levels: { + roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + }, + merge_access_levels: { + roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + } + } + end + + def open_branches + branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } + { open_branches: branches } + end + + def load_gon_index + gon.push(open_branches.merge(access_levels_options)) + end + end + end +end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 33379659d73..e13f0bde315 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController @tags = TagsFinder.new(@repository, params).execute @tags = Kaminari.paginate_array(@tags).page(params[:page]) - @releases = project.releases.where(tag: @tags.map(&:name)) + tag_names = @tags.map(&:name) + @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names) + @releases = project.releases.where(tag: tag_names) end def show @@ -41,13 +43,27 @@ class Projects::TagsController < Projects::ApplicationController end def destroy - Tags::DestroyService.new(project, current_user).execute(params[:id]) + result = Tags::DestroyService.new(project, current_user).execute(params[:id]) respond_to do |format| - format.html do - redirect_to namespace_project_tags_path(@project.namespace, @project) + if result[:status] == :success + format.html do + redirect_to namespace_project_tags_path(@project.namespace, @project) + end + + format.js + else + @error = result[:message] + + format.html do + redirect_to namespace_project_tags_path(@project.namespace, @project), + alert: @error + end + + format.js do + render status: :unprocessable_entity + end end - format.js end end end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index b2c11ea4156..c47198c5eb6 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,5 +1,8 @@ class Projects::TriggersController < Projects::ApplicationController before_action :authorize_admin_build! + before_action :authorize_manage_trigger!, except: [:index, :create] + before_action :authorize_admin_trigger!, only: [:edit, :update] + before_action :trigger, only: [:take_ownership, :edit, :update, :destroy] layout 'project_settings' @@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController end def create - @trigger = project.triggers.new - @trigger.save + @trigger = project.triggers.create(create_params.merge(owner: current_user)) if @trigger.valid? - redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.' + flash[:notice] = 'Trigger was created successfully.' else - @triggers = project.triggers.select(&:persisted?) - render action: "show" + flash[:alert] = 'You could not create a new trigger.' + end + + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + def take_ownership + if trigger.update(owner: current_user) + flash[:notice] = 'Trigger was re-assigned.' + else + flash[:alert] = 'You could not take ownership of trigger.' + end + + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + def edit + end + + def update + if trigger.update(update_params) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.' + else + render action: "edit" end end def destroy - trigger.destroy - flash[:alert] = "Trigger removed" + if trigger.destroy + flash[:notice] = "Trigger removed." + else + flash[:alert] = "Could not remove the trigger." + end redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end private + def authorize_manage_trigger! + access_denied! unless can?(current_user, :manage_trigger, trigger) + end + + def authorize_admin_trigger! + access_denied! unless can?(current_user, :admin_trigger, trigger) + end + def trigger - @trigger ||= project.triggers.find(params[:id]) + @trigger ||= project.triggers.find(params[:id]) || render_404 + end + + def create_params + params.require(:trigger).permit(:description) + end + + def update_params + params.require(:trigger).permit(:description) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 2d8064c9878..f210f7e61d2 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,5 +1,3 @@ -require 'project_wiki' - class Projects::WikisController < Projects::ApplicationController before_action :authorize_read_wiki! before_action :authorize_create_wiki!, only: [:edit, :create, :history] @@ -47,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :create_wiki, @project) @page = @project_wiki.find_page(params[:id]) + @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) - if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) + if @page.valid? redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index acca821782c..47f7e0b1b28 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -117,7 +117,7 @@ class ProjectsController < Projects::ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).async_execute - flash[:alert] = "Project '#{@project.name}' will be deleted." + flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted." redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex @@ -267,8 +267,9 @@ class ProjectsController < Projects::ApplicationController @project_wiki = @project.wiki @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) - @issues = issues_collection - @issues = @issues.page(params[:page]) + @issues = issues_collection.page(params[:page]) + @collection_type = 'Issue' + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) end render :show @@ -314,7 +315,8 @@ class ProjectsController < Projects::ApplicationController :name, :namespace_id, :only_allow_merge_if_all_discussions_are_resolved, - :only_allow_merge_if_build_succeeds, + :only_allow_merge_if_pipeline_succeeds, + :printing_merge_request_link_enabled, :path, :public_builds, :request_access_enabled, diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index db2817fadf6..1b4545e4a49 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -8,7 +8,9 @@ # `DashboardController#show`, which is the default. class RootController < Dashboard::ProjectsController skip_before_action :authenticate_user!, only: [:index] - before_action :redirect_to_custom_dashboard, only: [:index] + + before_action :redirect_unlogged_user, if: -> { current_user.nil? } + before_action :redirect_logged_user, if: -> { current_user.present? } def index super @@ -16,23 +18,38 @@ class RootController < Dashboard::ProjectsController private - def redirect_to_custom_dashboard - return redirect_to new_user_session_path unless current_user + def redirect_unlogged_user + if redirect_to_home_page_url? + redirect_to(current_application_settings.home_page_url) + else + redirect_to(new_user_session_path) + end + end + def redirect_logged_user case current_user.dashboard when 'stars' flash.keep - redirect_to starred_dashboard_projects_path + redirect_to(starred_dashboard_projects_path) when 'project_activity' - redirect_to activity_dashboard_path + redirect_to(activity_dashboard_path) when 'starred_project_activity' - redirect_to activity_dashboard_path(filter: 'starred') + redirect_to(activity_dashboard_path(filter: 'starred')) when 'groups' - redirect_to dashboard_groups_path + redirect_to(dashboard_groups_path) when 'todos' - redirect_to dashboard_todos_path - else - return + redirect_to(dashboard_todos_path) end end + + def redirect_to_home_page_url? + # If user is not signed-in and tries to access root_path - redirect him to landing page + # Don't redirect to the default URL to prevent endless redirections + return false unless current_application_settings.home_page_url.present? + + home_page_url = current_application_settings.home_page_url.chomp('/') + root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] + + root_urls.exclude?(home_page_url) + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 93a180b9036..7d81c96262f 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -15,11 +15,12 @@ class SessionsController < Devise::SessionsController def new set_minimum_password_length - if Gitlab.config.ldap.enabled - @ldap_servers = Gitlab::LDAP::Config.servers - else - @ldap_servers = [] - end + @ldap_servers = + if Gitlab.config.ldap.enabled + Gitlab::LDAP::Config.servers + else + [] + end super end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2d26718873f..f3fd3da8b20 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -28,8 +28,9 @@ class SnippetsController < ApplicationController @snippets = SnippetsFinder.new.execute(current_user, { filter: :by_user, user: @user, - scope: params[:scope] }). - page(params[:page]) + scope: params[:scope] + }) + .page(params[:page]) render 'index' else diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 509f4f412ca..f1bfd574f04 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -14,6 +14,8 @@ class UploadsController < ApplicationController end disposition = uploader.image? ? 'inline' : 'attachment' + + expires_in 0.seconds, must_revalidate: true, private: true send_file uploader.file.path, disposition: disposition end diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 9f2206346ce..fce3775f40e 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -1,4 +1,4 @@ -class GroupMembersFinder < Projects::ApplicationController +class GroupMembersFinder def initialize(group) @group = group end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 206c92fe82a..f7ebb1807d7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,7 +19,7 @@ # iids: integer[] # class IssuableFinder - NONE = '0' + NONE = '0'.freeze attr_accessor :current_user, :params @@ -33,15 +33,17 @@ class IssuableFinder items = by_scope(items) items = by_state(items) items = by_group(items) - items = by_project(items) items = by_search(items) - items = by_milestone(items) items = by_assignee(items) items = by_author(items) - items = by_label(items) items = by_due_date(items) items = by_non_archived(items) items = by_iids(items) + items = by_milestone(items) + items = by_label(items) + + # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far + items = by_project(items) sort(items) end @@ -107,8 +109,7 @@ class IssuableFinder @project = project end - def projects - return @projects if defined?(@projects) + def projects(items = nil) return @projects = project if project? projects = @@ -117,7 +118,7 @@ class IssuableFinder elsif group GroupProjectsFinder.new(group).execute(current_user) else - ProjectsFinder.new.execute(current_user) + projects_finder.execute(current_user, item_project_ids(items)) end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @@ -257,9 +258,9 @@ class IssuableFinder def by_project(items) items = if project? - items.of_projects(projects).references_project - elsif projects - items.merge(projects.reorder(nil)).join_project + items.of_projects(projects(items)).references_project + elsif projects(items) + items.merge(projects(items).reorder(nil)).join_project else items.none end @@ -309,18 +310,25 @@ class IssuableFinder params[:milestone_title] == Milestone::Upcoming.name end + def filter_by_started_milestone? + params[:milestone_title] == Milestone::Started.name + end + def by_milestone(items) if milestones? if filter_by_no_milestone? items = items.left_joins_milestones.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) + elsif filter_by_started_milestone? + items = items.left_joins_milestones.where('milestones.start_date <= NOW()') else items = items.with_milestone(params[:milestone_title]) + items_projects = projects(items) - if projects - items = items.where(milestones: { project_id: projects }) + if items_projects + items = items.where(milestones: { project_id: items_projects }) end end end @@ -334,9 +342,10 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names, params[:sort]) + items_projects = projects(items) - if projects - label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) + if items_projects + label_ids = LabelsFinder.new(current_user, project_ids: items_projects).execute(skip_authorization: true).select(:id) items = items.where(labels: { id: label_ids }) end end @@ -396,4 +405,8 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end + + def projects_finder + @projects_finder ||= ProjectsFinder.new + end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index f542f72a386..08713272947 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -41,4 +41,8 @@ class IssuesFinder < IssuableFinder user_id: user.id, project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) end + + def item_project_ids(items) + items&.reorder(nil)&.select(:project_id) + end end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 702944404f5..af24045886e 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -1,13 +1,35 @@ -class MembersFinder < Projects::ApplicationController - def initialize(project_members, project_group) - @project_members = project_members - @project_group = project_group +class MembersFinder + attr_reader :project, :current_user, :group + + def initialize(project, current_user) + @project = project + @current_user = current_user + @group = project.group + end + + def execute + project_members = project.project_members + project_members = project_members.non_invite unless can?(current_user, :admin_project, project) + wheres = ["members.id IN (#{project_members.select(:id).to_sql})"] + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id) + + group_members = GroupMembersFinder.new(group).execute + group_members = group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, group) + + wheres << "members.id IN (#{group_members.select(:id).to_sql})" + end + + Member.where(wheres.join(' OR ')) end - def execute(current_user) - non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) - group_members = @project_group.group_members.where.not(user_id: non_null_user_ids) - group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group) - group_members + def can?(*args) + Ability.allowed?(*args) end end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index b76ca389f38..1eec45d9cb5 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -20,4 +20,10 @@ class MergeRequestsFinder < IssuableFinder def klass MergeRequest end + + private + + def item_project_ids(items) + items&.reorder(nil)&.select(:target_project_id) + end end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 4bd8c83081a..6630c6384f2 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -28,11 +28,12 @@ class NotesFinder private def init_collection - if @params[:target_id] - @notes = on_target(@params[:target_type], @params[:target_id]) - else - @notes = notes_of_any_type - end + @notes = + if @params[:target_id] + on_target(@params[:target_type], @params[:target_id]) + else + notes_of_any_type + end end def notes_of_any_type diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb new file mode 100644 index 00000000000..760166b453f --- /dev/null +++ b/app/finders/personal_access_tokens_finder.rb @@ -0,0 +1,45 @@ +class PersonalAccessTokensFinder + attr_accessor :params + + delegate :build, :find, :find_by, to: :execute + + def initialize(params = {}) + @params = params + end + + def execute + tokens = PersonalAccessToken.all + tokens = by_user(tokens) + tokens = by_impersonation(tokens) + by_state(tokens) + end + + private + + def by_user(tokens) + return tokens unless @params[:user] + tokens.where(user: @params[:user]) + end + + def by_impersonation(tokens) + case @params[:impersonation] + when true + tokens.with_impersonation + when false + tokens.without_impersonation + else + tokens + end + end + + def by_state(tokens) + case @params[:state] + when 'active' + tokens.active + when 'inactive' + tokens.inactive + else + tokens + end + end +end diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index 32aea75486d..a9172f6767f 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -10,7 +10,11 @@ class PipelinesFinder scoped_pipelines = case scope when 'running' - pipelines.running_or_pending + pipelines.running + when 'pending' + pipelines.pending + when 'finished' + pipelines.finished when 'branches' from_ids(ids_for_ref(branches)) when 'tags' diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index a93a63bdb9b..13d33a1c31b 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -13,7 +13,7 @@ # class TodosFinder - NONE = '0' + NONE = '0'.freeze attr_accessor :current_user, :params @@ -24,6 +24,7 @@ class TodosFinder def execute items = current_user.todos + items = include_associations(items) items = by_action_id(items) items = by_action(items) items = by_author(items) @@ -38,6 +39,17 @@ class TodosFinder private + def include_associations(items) + return items unless params[:include_associations] + + items.includes( + [ + target: { project: [:route, namespace: :route] }, + author: { namespace: :route }, + ] + ) + end + def action_id? action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) end @@ -99,7 +111,7 @@ class TodosFinder end def type? - type.present? && ['Issue', 'MergeRequest'].include?(type) + type.present? && %w(Issue MergeRequest).include?(type) end def type diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6db813d4a02..a3213581498 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -69,11 +69,12 @@ module ApplicationHelper end def avatar_icon(user_or_email = nil, size = nil, scale = 2) - if user_or_email.is_a?(User) - user = user_or_email - else - user = User.find_by_any_email(user_or_email.try(:downcase)) - end + user = + if user_or_email.is_a?(User) + user_or_email + else + User.find_by_any_email(user_or_email.try(:downcase)) + end if user user.avatar_url(size) || default_avatar @@ -166,7 +167,7 @@ module ApplicationHelper css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - element = content_tag :time, time.to_s, + element = content_tag :time, time.strftime("%b %d, %Y"), class: css_classes, title: time.to_time.in_time_zone.to_s(:medium), datetime: time.to_time.getutc.iso8601, diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 60485160495..ca326dd0627 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -1,28 +1,15 @@ module ApplicationSettingsHelper - def gravatar_enabled? - current_application_settings.gravatar_enabled? - end - - def signup_enabled? - current_application_settings.signup_enabled? - end - - def signin_enabled? - current_application_settings.signin_enabled? - end + delegate :gravatar_enabled?, + :signup_enabled?, + :signin_enabled?, + :akismet_enabled?, + :koding_enabled?, + to: :current_application_settings def user_oauth_applications? current_application_settings.user_oauth_applications end - def askimet_enabled? - current_application_settings.akismet_enabled? - end - - def koding_enabled? - current_application_settings.koding_enabled? - end - def allowed_protocols_present? current_application_settings.enabled_git_access_protocol.present? end @@ -94,8 +81,8 @@ module ApplicationSettingsHelper end def repository_storages_options_for_select - options = Gitlab.config.repositories.storages.map do |name, path| - ["#{name} - #{path}", name] + options = Gitlab.config.repositories.storages.map do |name, storage| + ["#{name} - #{storage['path']}", name] end options_for_select(options, @application_setting.repository_storages) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 311a70725ab..8631bc54509 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -153,16 +153,17 @@ module BlobHelper # Because we are opionated we set the cache headers ourselves. response.cache_control[:public] = @project.public? - if @ref && @commit && @ref == @commit.id - # This is a link to a commit by its commit SHA. That means that the blob - # is immutable. The only reason to invalidate the cache is if the commit - # was deleted or if the user lost access to the repository. - response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE - else - # A branch or tag points at this blob. That means that the expected blob - # value may change over time. - response.cache_control[:max_age] = Blob::CACHE_TIME - end + response.cache_control[:max_age] = + if @ref && @commit && @ref == @commit.id + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + Blob::CACHE_TIME_IMMUTABLE + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + Blob::CACHE_TIME + end response.etag = @blob.id !stale @@ -202,4 +203,18 @@ module BlobHelper 'blob-language' => @blob && @blob.language.try(:ace_mode) } end + + def copy_file_path_button(file_path) + clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + end + + def copy_blob_content_button(blob) + return if markup?(blob.name) + + clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") + end + + def open_raw_file_button(path) + link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } + end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index ff937b5ebd2..2fcb7a59fc3 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -12,7 +12,14 @@ module BuildsHelper build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_status: @build.status, build_stage: @build.stage, - log_state: @build.trace_with_state[:state].to_s + log_state: '' + } + end + + def build_failed_issue_options + { + title: "Build Failed ##{@build.id}", + description: namespace_project_build_url(@project.namespace, @project, @build) } end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 4c7c16d694c..0b30471f2ae 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -19,7 +19,7 @@ module ButtonHelper title = data[:title] || 'Copy to clipboard' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, - icon('clipboard'), + icon('clipboard', 'aria-hidden': 'true'), class: "btn #{css_class}", data: data, type: :button, @@ -34,7 +34,7 @@ module ButtonHelper content_tag (append_link ? :a : :span), protocol, class: klass, - href: (project.http_url_to_repo if append_link), + href: (project.http_url_to_repo(current_user) if append_link), data: { html: true, placement: placement, diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 94f3b480178..2de9e0de310 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -15,6 +15,8 @@ module CiStatusHelper 'passed' when 'success_with_warnings' 'passed with warnings' + when 'manual' + 'waiting for manual action' else status end @@ -48,6 +50,8 @@ module CiStatusHelper 'icon_status_created' when 'skipped' 'icon_status_skipped' + when 'manual' + 'icon_status_manual' else 'icon_status_canceled' end @@ -55,6 +59,24 @@ module CiStatusHelper custom_icon(icon_name) end + def pipeline_status_cache_key(pipeline_status) + "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" + end + + def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left') + project = pipeline_status.project + path = pipelines_namespace_project_commit_path( + project.namespace, + project, + pipeline_status.sha) + + render_status_with_link( + 'commit', + pipeline_status.status, + path, + tooltip_placement: tooltip_placement) + end + def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left') project = commit.project path = pipelines_namespace_project_commit_path( diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 8aad39e148b..cef624430da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -211,7 +211,7 @@ module CommitsHelper external_url = environment.external_url_for(diff_new_path, commit_sha) return unless external_url - link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do + link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do icon('external-link') end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 2843ad96efa..f927cfc998f 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -1,4 +1,6 @@ module EmailsHelper + include AppearancesHelper + # Google Actions # https://developers.google.com/gmail/markup/reference/go-to-action def email_action(url) @@ -22,7 +24,7 @@ module EmailsHelper def action_title(url) return unless url - ["merge_requests", "issues", "commit"].each do |action| + %w(merge_requests issues commit).each do |action| if url.split("/").include?(action) return "View #{action.humanize.singularize}" end @@ -49,4 +51,19 @@ module EmailsHelper msg = "This link is valid for #{password_reset_token_valid_time}. " msg << "After it expires, you can #{link_tag}." end + + def header_logo + if brand_item && brand_item.header_logo? + image_tag( + brand_item.header_logo, + style: 'height: 50px' + ) + else + image_tag( + image_url('mailers/gitlab_header_logo.gif'), + size: "55x50", + alt: "GitLab" + ) + end + end end diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb new file mode 100644 index 00000000000..482f68f412b --- /dev/null +++ b/app/helpers/emoji_helper.rb @@ -0,0 +1,5 @@ +module EmojiHelper + def emoji_icon(*args) + raw Gitlab::Emoji.gl_emoji_tag(*args) + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 362046c0270..fb872a13f74 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -162,7 +162,12 @@ module EventsHelper def event_note(text, options = {}) text = first_line_in_markdown(text, 150, options) - sanitize(text, tags: %w(a img b pre code p span)) + + sanitize( + text, + tags: %w(a img gl-emoji b pre code p span), + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version'] + ) end def event_commit_title(message) diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 2b1f3825adc..7bd212a3ef9 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,20 +1,33 @@ module ExploreHelper def filter_projects_path(options = {}) exist_opts = { - sort: params[:sort], + sort: params[:sort] || @sort, scope: params[:scope], group: params[:group], tag: params[:tag], visibility_level: params[:visibility_level], + name: params[:name], + personal: params[:personal], + archived: params[:archived], + shared: params[:shared], + namespace_id: params[:namespace_id], } - options = exist_opts.merge(options) - path = request.path - path << "?#{options.to_param}" - path + options = exist_opts.merge(options).delete_if { |key, value| value.blank? } + request_path_with_options(options) + end + + def filter_groups_path(options = {}) + request_path_with_options(options) end def explore_controller? controller.class.name.split("::").first == "Explore" end + + private + + def request_path_with_options(options = {}) + request.path + "?#{options.to_param}" + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 6d365ea9251..cd442237086 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -172,7 +172,9 @@ module GitlabMarkdownHelper # text hasn't already been truncated, then append "..." to the node contents # and return true. Otherwise return false. def truncate_if_block(node, truncated) - if node.element? && node.description.block? && !truncated + return true if truncated + + if node.element? && (node.description&.block? || node.matches?('pre > code > .line')) node.inner_html = "#{node.inner_html}..." if node.next_sibling true else diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f16a63e2178..e9b7cbbad6a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -74,6 +74,10 @@ module GitlabRoutingHelper namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) end + def environment_metrics_path(environment, *args) + metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 926c9703628..a6014088e92 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,17 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) + @has_group_title = true full_title = '' group.ancestors.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent)) - full_title += ' / '.html_safe + full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + full_title += '<span class="hidable"> / </span>'.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group)) - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name - content_tag :span do + content_tag :span, class: 'group-title' do full_title.html_safe end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a0642a1894b..a57b5a8fea5 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -7,7 +7,7 @@ module ImportHelper def provider_project_link(provider, path_with_namespace) url = __send__("#{provider}_project_url", path_with_namespace) - link_to path_with_namespace, url, target: '_blank' + link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end private diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 03354c235eb..a777db2826b 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,6 +1,8 @@ module IssuablesHelper + include GitlabRoutingHelper + def sidebar_gutter_toggle_icon - sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right') + sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) end def sidebar_gutter_collapsed_class @@ -23,7 +25,7 @@ module IssuablesHelper def issuable_json_path(issuable) project = issuable.project - if issuable.kind_of?(MergeRequest) + if issuable.is_a?(MergeRequest) namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json) else namespace_project_issue_path(project.namespace, project, issuable.iid, :json) @@ -52,7 +54,7 @@ module IssuablesHelper field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, - namespace_path: ref_project.namespace.path + namespace_path: ref_project.namespace.full_path } } @@ -88,15 +90,33 @@ module IssuablesHelper end def milestone_dropdown_label(milestone_title, default_label = "Milestone") - if milestone_title == Milestone::Upcoming.name - milestone_title = Milestone::Upcoming.title - end + title = + case milestone_title + when Milestone::Upcoming.name then Milestone::Upcoming.title + when Milestone::Started.name then Milestone::Started.title + else milestone_title.presence + end - h(milestone_title.presence || default_label) + h(title || default_label) + end + + def to_url_reference(issuable) + case issuable + when Issue + link_to issuable.to_reference, issue_url(issuable) + when MergeRequest + link_to issuable.to_reference, merge_request_url(issuable) + else + issuable.to_reference + end end def issuable_meta(issuable, project, text) - output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output = content_tag(:strong, class: "identifier") do + concat("#{text} ") + concat(to_url_reference(issuable)) + end + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) @@ -198,7 +218,7 @@ module IssuablesHelper @counts[issuable_type][state] end - IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY def issuables_state_counter_cache_key(issuable_type, state) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a2d21b67a77..6978b0c89fd 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -87,34 +87,6 @@ module IssuesHelper icon('eye-slash') if issue.confidential? end - def emoji_icon(name, unicode = nil, aliases = [], sprite: true) - unicode ||= Gitlab::Emoji.emoji_filename(name) rescue "" - - data = { - aliases: aliases.join(" "), - emoji: name, - unicode_name: unicode - } - - if sprite - # Emoji icons for the emoji menu, these use a spritesheet. - content_tag :div, "", - class: "icon emoji-icon emoji-#{unicode}", - title: name, - data: data - else - # Emoji icons displayed separately, used for the awards already given - # to an issue or merge request. - content_tag :img, "", - class: "icon emoji", - title: name, - height: "20px", - width: "20px", - src: url_to_image("#{unicode}.png"), - data: data - end - end - def award_user_list(awards, current_user, limit: 10) names = awards.map do |award| award.user == current_user ? 'You' : award.user.name @@ -162,6 +134,20 @@ module IssuesHelper options_from_collection_for_select(options, 'name', 'title', params[:due_date]) end + def link_to_discussions_to_resolve(merge_request, single_discussion = nil) + link_text = merge_request.to_reference + link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion + + path = if single_discussion + Gitlab::UrlBuilder.build(single_discussion.first_note) + else + project = merge_request.project + namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + link_to link_text, path + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 320dd89c9d3..68c09c922a6 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -2,6 +2,7 @@ module JavascriptHelper def page_specific_javascript_tag(js) javascript_include_tag asset_path(js) end + def page_specific_javascript_bundle_tag(js) javascript_include_tag(*webpack_asset_paths(js)) end diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb index 49ac12db832..27ff4051c8d 100644 --- a/app/helpers/mattermost_helper.rb +++ b/app/helpers/mattermost_helper.rb @@ -1,9 +1,7 @@ module MattermostHelper def mattermost_teams_options(teams) - teams_options = teams.map do |id, options| - [options['display_name'] || options['name'], id] + teams.map do |team| + [team['display_name'] || team['name'], team['id']] end - - teams_options.compact.unshift(['Select team...', '0']) end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 7d8505d704e..38be073c8dc 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -146,7 +146,7 @@ module MergeRequestsHelper def merge_params(merge_request) { - merge_when_build_succeeds: true, + merge_when_pipeline_succeeds: true, should_remove_source_branch: true, sha: merge_request.diff_head_sha }.merge(merge_params_ee(merge_request)) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 729928ce1dd..5053b937c02 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -82,12 +82,13 @@ module MilestonesHelper def milestone_remaining_days(milestone) if milestone.expired? content_tag(:strong, 'Past due') - elsif milestone.due_date - days = milestone.remaining_days - content = content_tag(:strong, days) - content << " #{'day'.pluralize(days)} remaining" elsif milestone.upcoming? content_tag(:strong, 'Upcoming') + elsif milestone.due_date + time_ago = time_ago_in_words(milestone.due_date) + content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } + content.slice!("about ") + content << " remaining" elsif milestone.start_date && milestone.start_date.past? days = milestone.elapsed_days content = content_tag(:strong, days) @@ -97,7 +98,7 @@ module MilestonesHelper def milestone_date_range(milestone) if milestone.start_date && milestone.due_date - "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}" + "#{milestone.start_date.to_s(:medium)}–#{milestone.due_date.to_s(:medium)}" elsif milestone.due_date if milestone.due_date.past? "expired on #{milestone.due_date.to_s(:medium)}" diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index dc5ae8edbb2..2e3a15bc1b9 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -33,7 +33,7 @@ module NamespacesHelper end def namespace_icon(namespace, size = 40) - if namespace.kind_of?(Group) + if namespace.is_a?(Group) group_icon(namespace) else avatar_icon(namespace.owner.email, size) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index c1523b4dabf..a8f167cbff2 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -16,6 +16,7 @@ module NavHelper "page-gutter build-sidebar right-sidebar-expanded" elsif current_path?('wikis#show') || current_path?('wikis#edit') || + current_path?('wikis#update') || current_path?('wikis#history') || current_path?('wikis#git_access') "page-gutter wiki-sidebar right-sidebar-expanded" diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index c3a08d76318..243ef39ef61 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -35,9 +35,8 @@ module PreferencesHelper def project_view_choices [ - ['Readme (default)', :readme], - ['Activity view', :activity], - ['Files view', :files] + ['Files and Readme (default)', :files], + ['Activity', :activity] ] end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index eb98204285d..bd0c2cd661e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -150,6 +150,22 @@ module ProjectsHelper ).html_safe end + def link_to_autodeploy_doc + link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank' + end + + def autodeploy_flash_notice(branch_name) + "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \ + choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe + end + + def project_list_cache_key(project) + key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3'] + key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? + + key + end + private def repo_children_classes(field) @@ -232,7 +248,7 @@ module ProjectsHelper when 'ssh' project.ssh_url_to_repo else - project.http_url_to_repo + project.http_url_to_repo(current_user) end end diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb new file mode 100644 index 00000000000..ea5d2932ef4 --- /dev/null +++ b/app/helpers/rss_helper.rb @@ -0,0 +1,5 @@ +module RssHelper + def rss_url_options + { format: :atom, private_token: current_user.try(:private_token) } + end +end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index ff787fb4131..959ee310867 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -16,7 +16,8 @@ module SortingHelper sort_value_oldest_signin => sort_title_oldest_signin, sort_value_downvotes => sort_title_downvotes, sort_value_upvotes => sort_title_upvotes, - sort_value_priority => sort_title_priority + sort_value_priority => sort_title_priority, + sort_value_label_priority => sort_title_label_priority } end @@ -30,7 +31,7 @@ module SortingHelper } if current_controller?('admin/projects') - options.merge!(sort_value_largest_repo => sort_title_largest_repo) + options[sort_value_largest_repo] = sort_title_largest_repo end options @@ -53,6 +54,10 @@ module SortingHelper 'Priority' end + def sort_title_label_priority + 'Label priority' + end + def sort_title_oldest_updated 'Oldest updated' end @@ -161,6 +166,10 @@ module SortingHelper 'priority' end + def sort_value_label_priority + 'label_priority' + end + def sort_value_oldest_updated 'updated_asc' end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 9a748aaaf33..fb95f2b565e 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -37,8 +37,8 @@ module SubmoduleHelper end def self_url?(url, namespace, project) - return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/', - project, '.git' ].join('') + return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/', + project, '.git'].join('') url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) end @@ -48,8 +48,8 @@ module SubmoduleHelper end def standard_links(host, namespace, project, commit) - base = [ 'https://', host, '/', namespace, '/', project ].join('') - [base, [ base, '/tree/', commit ].join('')] + base = ['https://', host, '/', namespace, '/', project].join('') + [base, [base, '/tree/', commit].join('')] end def relative_self_links(url, commit) diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 547f6258909..1a55ee05996 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -99,7 +99,7 @@ module TabHelper return 'active' end - if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name + if %w(services hooks deploy_keys protected_branches).include? controller.controller_name "active" end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index c52afd6db1c..847a8fdfca6 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -39,9 +39,13 @@ module TodosHelper namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) else - path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] - - path.unshift(:pipelines) if todo.build_failed? + if todo.build_failed? + # associated namespace and route would be loaded from the db again if todo.project was used + project = todo.target.project + path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target] + else + path = [todo.target] + end polymorphic_path(path, anchor: anchor) end @@ -99,8 +103,7 @@ module TodosHelper end def todo_projects_options - projects = current_user.authorized_projects.sorted_by_activity.non_archived - projects = projects.includes(:namespace) + projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route projects = projects.map do |project| { id: project.id, text: project.name_with_namespace } @@ -150,6 +153,6 @@ module TodosHelper private def show_todo_state?(todo) - (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state) + (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) end end diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index b0135ea2e95..a48d4475e97 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,9 +1,9 @@ module TriggersHelper def builds_trigger_url(project_id, ref: nil) if ref.nil? - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" + "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline" else - "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" + "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/ref/#{ref}/trigger/pipeline" end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index fc93acfe63e..169cedeb796 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -89,13 +89,9 @@ module VisibilityLevelHelper current_application_settings.restricted_visibility_levels || [] end - def default_project_visibility - current_application_settings.default_project_visibility - end - - def default_group_visibility - current_application_settings.default_group_visibility - end + delegate :default_project_visibility, + :default_group_visibility, + to: :current_application_settings def skip_level?(form_model, level) form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level) diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb deleted file mode 100644 index 3853af6201a..00000000000 --- a/app/mailers/emails/builds.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Emails - module Builds - def build_fail_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - - add_project_headers - add_build_headers('failed') - - mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) - end - - def build_success_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - - add_project_headers - add_build_headers('success') - mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) - end - - private - - def add_build_headers(status) - headers['X-GitLab-Build-Id'] = @build.id - headers['X-GitLab-Build-Ref'] = @build.ref - headers['X-GitLab-Build-Status'] = status.to_s - end - end -end diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 9460a6cd2be..f9f45ab987b 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -22,8 +22,8 @@ module Emails mail(bcc: recipients, subject: pipeline_subject(status), skip_premailer: true) do |format| - format.html { render layout: false } - format.text + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 5b9226a6b81..14df6f8f0a3 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -6,7 +6,6 @@ class Notify < BaseMailer include Emails::Notes include Emails::Projects include Emails::Profile - include Emails::Builds include Emails::Pipelines include Emails::Members diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 21db2fe04a0..22a9f5da646 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,10 +1,11 @@ class RepositoryCheckMailer < BaseMailer def notify(failed_count) - if failed_count == 1 - @message = "One project failed its last repository check" - else - @message = "#{failed_count} projects failed their last repository check" - end + @message = + if failed_count == 1 + "One project failed its last repository check" + else + "#{failed_count} projects failed their last repository check" + end mail( to: User.admins.pluck(:email), diff --git a/app/models/ability.rb b/app/models/ability.rb index ad6c588202e..f3692a5a067 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -56,15 +56,16 @@ class Ability end end - def allowed?(user, action, subject) + def allowed?(user, action, subject = :global) allowed(user, subject).include?(action) end - def allowed(user, subject) + def allowed(user, subject = :global) + return BasePolicy::RuleSet.none if subject.nil? return uncached_allowed(user, subject) unless RequestStore.active? user_key = user ? user.id : 'anonymous' - subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global' + subject_key = subject == :global ? 'global' : "#{subject.class.name}/#{subject.id}" key = "/ability/#{user_key}/#{subject_key}" RequestStore[key] ||= uncached_allowed(user, subject).freeze end diff --git a/app/models/appearance.rb b/app/models/appearance.rb index e4106e1c2e9..c79326e8427 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + has_many :uploads, as: :model, dependent: :destroy end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b94a71e1ea7..9d01a70c77d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -6,7 +6,7 @@ class ApplicationSetting < ActiveRecord::Base add_authentication_token_field :health_check_access_token add_authentication_token_field :container_registry_access_token - CACHE_KEY = 'application_setting.last' + CACHE_KEY = 'application_setting.last'.freeze DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace | # or \s # any whitespace character @@ -65,6 +65,16 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :akismet_enabled + validates :unique_ips_limit_per_user, + numericality: { greater_than_or_equal_to: 1 }, + presence: true, + if: :unique_ips_limit_enabled + + validates :unique_ips_limit_time_window, + numericality: { greater_than_or_equal_to: 0 }, + presence: true, + if: :unique_ips_limit_enabled + validates :koding_url, presence: true, if: :koding_enabled @@ -77,6 +87,12 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :max_artifacts_size, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :default_artifacts_expire_in, presence: true, duration: true + validates :container_registry_token_expire_delay, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -149,6 +165,8 @@ class ApplicationSetting < ActiveRecord::Base end def self.current + ensure_cache_setup + Rails.cache.fetch(CACHE_KEY) do ApplicationSetting.last end @@ -162,22 +180,34 @@ class ApplicationSetting < ActiveRecord::Base end def self.cached + ensure_cache_setup Rails.cache.fetch(CACHE_KEY) end + def self.ensure_cache_setup + # This is a workaround for a Rails bug that causes attribute methods not + # to be loaded when read from cache: https://github.com/rails/rails/issues/27348 + ApplicationSetting.define_attribute_methods + end + def self.defaults_ce { after_sign_up_text: nil, akismet_enabled: false, container_registry_token_expire_delay: 5, + default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], disabled_oauth_sign_in_sources: [], domain_whitelist: Settings.gitlab['domain_whitelist'], gravatar_enabled: Settings.gravatar['enabled'], help_page_text: nil, + unique_ips_limit_per_user: 10, + unique_ips_limit_time_window: 3600, + unique_ips_limit_enabled: false, housekeeping_bitmaps_enabled: true, housekeeping_enabled: true, housekeeping_full_repack_period: 50, @@ -203,9 +233,9 @@ class ApplicationSetting < ActiveRecord::Base sign_in_text: nil, signin_enabled: Settings.gitlab['signin_enabled'], signup_enabled: Settings.gitlab['signup_enabled'], + terminal_max_session_time: 0, two_factor_grace_period: 48, - user_default_external: false, - terminal_max_session_time: 0 + user_default_external: false } end @@ -217,6 +247,14 @@ class ApplicationSetting < ActiveRecord::Base create(defaults) end + def self.human_attribute_name(attr, _options = {}) + if attr == :default_artifacts_expire_in + 'Default artifacts expiration' + else + super + end + end + def home_page_url_column_exist ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end @@ -264,6 +302,22 @@ class ApplicationSetting < ActiveRecord::Base self.repository_storages = [value] end + def default_project_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_snippet_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_group_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def restricted_visibility_levels=(levels) + super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) + end + # Choose one of the available repository storage options. Currently all have # equal weighting. def pick_repository_storage diff --git a/app/models/blob.rb b/app/models/blob.rb index ab92e820335..1376b86fdad 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -54,9 +54,13 @@ class Blob < SimpleDelegator UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) end - def to_partial_path + def to_partial_path(project) if lfs_pointer? - 'download' + if project.lfs_enabled? + 'download' + else + 'text' + end elsif image? || svg? 'image' elsif text? diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb new file mode 100644 index 00000000000..c52b6f15913 --- /dev/null +++ b/app/models/chat_team.rb @@ -0,0 +1,6 @@ +class ChatTeam < ActiveRecord::Base + validates :team_id, presence: true + validates :namespace, uniqueness: true + + belongs_to :namespace +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e018f8e7c4e..ad0be70c32a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -15,15 +15,17 @@ module Ci def persisted_environment @persisted_environment ||= Environment.find_by( name: expanded_environment_name, - project_id: gl_project_id + project: project ) end serialize :options serialize :yaml_variables, Gitlab::Serializer::Ci::Variables + delegate :name, to: :project, prefix: true + validates :coverage, numericality: true, allow_blank: true - validates_presence_of :ref + validates :ref, presence: true scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } @@ -53,15 +55,6 @@ module Ci pending.unstarted.order('created_at ASC').first end - def create_from(build) - new_build = build.dup - new_build.status = 'pending' - new_build.runner_id = nil - new_build.trigger_request_id = nil - new_build.token = nil - new_build.save - end - def retry(build, current_user) Ci::RetryBuildService .new(build.project, current_user) @@ -70,6 +63,10 @@ module Ci end state_machine :status do + event :actionize do + transition created: :manual + end + after_transition any => [:pending] do |build| build.run_after_commit do BuildQueueWorker.perform_async(id) @@ -101,16 +98,21 @@ module Ci .fabricate! end - def manual? - self.when == 'manual' - end - def other_actions pipeline.manual_actions.where.not(name: name) end def playable? - project.builds_enabled? && commands.present? && manual? && skipped? + project.builds_enabled? && has_commands? && + action? && manual? + end + + def action? + self.when == 'manual' + end + + def has_commands? + commands.present? end def play(current_user) @@ -129,7 +131,7 @@ module Ci end def retryable? - project.builds_enabled? && commands.present? && + project.builds_enabled? && has_commands? && (success? || failed? || canceled?) end @@ -221,7 +223,8 @@ module Ci def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) - .where(source_branch: ref, source_project_id: pipeline.gl_project_id) + .where(source_branch: ref, + source_project: pipeline.project) .reorder(iid: :asc) merge_requests.find do |merge_request| @@ -229,14 +232,6 @@ module Ci end end - def project_id - gl_project_id - end - - def project_name - project.name - end - def repo_url auth = "gitlab-ci-token:#{ensure_token!}@" project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| @@ -257,7 +252,7 @@ module Ci return unless regex matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.kind_of?(Array) + matches = matches.last if matches.is_a?(Array) coverage = matches.gsub(/\d+(\.\d+)?/).first if coverage.present? @@ -486,7 +481,7 @@ module Ci def artifacts_expire_in=(value) self.artifacts_expire_at = if value - Time.now + ChronicDuration.parse(value) + ChronicDuration.parse(value)&.seconds&.from_now end end @@ -519,10 +514,41 @@ module Ci ] end + def steps + [Gitlab::Ci::Build::Step.from_commands(self), + Gitlab::Ci::Build::Step.from_after_script(self)].compact + end + + def image + Gitlab::Ci::Build::Image.from_image(self) + end + + def services + Gitlab::Ci::Build::Image.from_services(self) + end + + def artifacts + [options[:artifacts]] + end + + def cache + [options[:cache]] + end + def credentials Gitlab::Ci::Build::Credentials::Factory.new(self).create! end + def dependencies + depended_jobs = depends_on_builds + + return depended_jobs unless options[:dependencies].present? + + depended_jobs.select do |job| + options[:dependencies].include?(job.name) + end + end + private def update_artifacts_size @@ -542,13 +568,38 @@ module Ci end def unscoped_project - @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) + @unscoped_project ||= Project.unscoped.find_by(id: project_id) end + CI_REGISTRY_USER = 'gitlab-ci-token'.freeze + def predefined_variables variables = [ { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_JOB_ID', value: id.to_s, public: true }, + { key: 'CI_JOB_NAME', value: name, public: true }, + { key: 'CI_JOB_STAGE', value: stage, public: true }, + { key: 'CI_JOB_TOKEN', value: token, public: false }, + { key: 'CI_COMMIT_SHA', value: sha, public: true }, + { key: 'CI_COMMIT_REF_NAME', value: ref, public: true }, + { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true }, + { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true }, + { key: 'CI_REGISTRY_PASSWORD', value: token, public: false }, + { key: 'CI_REPOSITORY_URL', value: repo_url, public: false } + ] + + variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag? + variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request + variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action? + variables.concat(legacy_variables) + end + + def legacy_variables + variables = [ { key: 'CI_BUILD_ID', value: id.to_s, public: true }, { key: 'CI_BUILD_TOKEN', value: token, public: false }, { key: 'CI_BUILD_REF', value: sha, public: true }, @@ -556,14 +607,12 @@ module Ci { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, - { key: 'CI_BUILD_STAGE', value: stage, public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true } + { key: 'CI_BUILD_STAGE', value: stage, public: true } ] - variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag? - variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request - variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual? + + variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag? + variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request + variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action? variables end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index dc4590a9923..f12be98c80c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -5,21 +5,22 @@ module Ci include Importable include AfterCommitQueue - self.table_name = 'ci_commits' - - belongs_to :project, foreign_key: :gl_project_id + belongs_to :project belongs_to :user has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id - validates_presence_of :sha, unless: :importing? - validates_presence_of :ref, unless: :importing? - validates_presence_of :status, unless: :importing? + delegate :id, to: :project, prefix: true + + validates :sha, presence: { unless: :importing? } + validates :ref, presence: { unless: :importing? } + validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? after_create :keep_around_commits, unless: :importing? + after_create :refresh_build_status_cache state_machine :status, initial: :created do event :enqueue do @@ -47,6 +48,10 @@ module Ci transition any - [:canceled] => :canceled end + event :block do + transition any - [:manual] => :manual + end + # IMPORTANT # Do not add any operations to this state_machine # Create a separate worker for each new operation @@ -93,8 +98,11 @@ module Ci .select("max(#{quoted_table_name}.id)") .group(:ref, :sha) - relation = ref ? where(ref: ref) : self - relation.where(id: max_id) + if ref + where(ref: ref, id: max_id.where(ref: ref)) + else + where(id: max_id) + end end def self.latest_status(ref = nil) @@ -105,6 +113,12 @@ module Ci success.latest(ref).order(id: :desc).first end + def self.latest_successful_for_refs(refs) + success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash| + hash[pipeline.ref] ||= pipeline + end + end + def self.truncate_sha(sha) sha[0...8] end @@ -135,7 +149,7 @@ module Ci status_sql = statuses.latest.where('stage=sg.stage').status_sql - warnings_sql = statuses.latest.select('COUNT(*) > 0') + warnings_sql = statuses.latest.select('COUNT(*)') .where('stage=sg.stage').failed_but_allowed.to_sql stages_with_statuses = CommitStatus.from(stages_query, :sg) @@ -150,10 +164,6 @@ module Ci builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end - def project_id - project.id - end - # For now the only user who participates is the user who triggered def participants(_current_user = nil) Array(user) @@ -320,8 +330,10 @@ module Ci when 'failed' then drop when 'canceled' then cancel when 'skipped' then skip + when 'manual' then block end end + refresh_build_status_cache end def predefined_variables @@ -363,6 +375,10 @@ module Ci .fabricate! end + def refresh_build_status_cache + Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed + end + private def pipeline_data diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb new file mode 100644 index 00000000000..048047d0e34 --- /dev/null +++ b/app/models/ci/pipeline_status.rb @@ -0,0 +1,86 @@ +# This class is not backed by a table in the main database. +# It loads the latest Pipeline for the HEAD of a repository, and caches that +# in Redis. +module Ci + class PipelineStatus + attr_accessor :sha, :status, :project, :loaded + + delegate :commit, to: :project + + def self.load_for_project(project) + new(project).tap do |status| + status.load_status + end + end + + def initialize(project, sha: nil, status: nil) + @project = project + @sha = sha + @status = status + end + + def has_status? + loaded? && sha.present? && status.present? + end + + def load_status + return if loaded? + + if has_cache? + load_from_cache + else + load_from_commit + store_in_cache + end + + self.loaded = true + end + + def load_from_commit + return unless commit + + self.sha = commit.sha + self.status = commit.status + end + + # We only cache the status for the HEAD commit of a project + # This status is rendered in project lists + def store_in_cache_if_needed + return unless sha + return delete_from_cache unless commit + store_in_cache if commit.sha == self.sha + end + + def load_from_cache + Gitlab::Redis.with do |redis| + self.sha, self.status = redis.hmget(cache_key, :sha, :status) + end + end + + def store_in_cache + Gitlab::Redis.with do |redis| + redis.mapped_hmset(cache_key, { sha: sha, status: status }) + end + end + + def delete_from_cache + Gitlab::Redis.with do |redis| + redis.del(cache_key) + end + end + + def has_cache? + Gitlab::Redis.with do |redis| + redis.exists(cache_key) + end + end + + def loaded? + self.loaded + end + + def cache_key + "projects/#{project.id}/build_status" + end + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 07a086b0aca..487ba61bc9c 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,12 +4,12 @@ module Ci RUNNER_QUEUE_EXPIRY_TIME = 60.minutes LAST_CONTACT_TIME = 1.hour.ago - AVAILABLE_SCOPES = %w[specific shared active paused online] - FORM_EDITABLE = %i[description tag_list active run_untagged locked] + AVAILABLE_SCOPES = %w[specific shared active paused online].freeze + FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze has_many :builds has_many :runner_projects, dependent: :destroy - has_many :projects, through: :runner_projects, foreign_key: :gl_project_id + has_many :projects, through: :runner_projects has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' @@ -24,7 +24,7 @@ module Ci scope :owned_or_shared, ->(project_id) do joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') - .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) + .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end scope :assignable_for, ->(project) do @@ -127,18 +127,15 @@ module Ci def tick_runner_queue SecureRandom.hex.tap do |new_update| - Gitlab::Redis.with do |redis| - redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) - end + ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update, + expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true) end end def ensure_runner_queue_value - Gitlab::Redis.with do |redis| - value = SecureRandom.hex - redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true) - redis.get(runner_queue_key) - end + new_value = SecureRandom.hex + ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value, + expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false) end def is_runner_queue_value_latest?(value) diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 1f9baeca5b1..5f01a0daae9 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,10 +1,10 @@ module Ci class RunnerProject < ActiveRecord::Base extend Ci::Model - + belongs_to :runner - belongs_to :project, foreign_key: :gl_project_id + belongs_to :project - validates_uniqueness_of :runner_id, scope: :gl_project_id + validates :runner_id, uniqueness: { scope: :project_id } end end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index ca74c91b062..e7d6b17d445 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -46,10 +46,10 @@ module Ci end def has_warnings? - if @warnings.nil? - statuses.latest.failed_but_allowed.any? + if @warnings.is_a?(Integer) + @warnings > 0 else - @warnings + statuses.latest.failed_but_allowed.any? end end end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 62889fe80d8..cba1d81a861 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -4,11 +4,12 @@ module Ci acts_as_paranoid - belongs_to :project, foreign_key: :gl_project_id + belongs_to :project + belongs_to :owner, class_name: "User" + has_many :trigger_requests, dependent: :destroy - validates_presence_of :token - validates_uniqueness_of :token + validates :token, presence: true, uniqueness: true before_validation :set_default_values @@ -25,7 +26,15 @@ module Ci end def short_token - token[0...10] + token[0...4] + end + + def legacy? + self.owner_id.blank? + end + + def can_access_project? + self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) end end end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 2c8698d8b5d..6c6586110c5 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -2,11 +2,11 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model - belongs_to :project, foreign_key: :gl_project_id + belongs_to :project validates :key, presence: true, - uniqueness: { scope: :gl_project_id }, + uniqueness: { scope: :project_id }, length: { maximum: 255 }, format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } diff --git a/app/models/commit.rb b/app/models/commit.rb index 46f06733da1..ce92cc369ad 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -22,12 +22,12 @@ class Commit DIFF_HARD_LIMIT_LINES = 50000 # The SHA can be between 7 and 40 hex characters. - COMMIT_SHA_PATTERN = '\h{7,40}' + COMMIT_SHA_PATTERN = '\h{7,40}'.freeze class << self def decorate(commits, project) commits.map do |commit| - if commit.kind_of?(Commit) + if commit.is_a?(Commit) commit else self.new(commit, project) @@ -105,7 +105,7 @@ class Commit end def diff_line_count - @diff_line_count ||= Commit::diff_line_count(raw_diffs) + @diff_line_count ||= Commit.diff_line_count(raw_diffs) @diff_line_count end @@ -122,11 +122,12 @@ class Commit def full_title return @full_title if @full_title - if safe_message.blank? - @full_title = no_commit_message - else - @full_title = safe_message.split("\n", 2).first - end + @full_title = + if safe_message.blank? + no_commit_message + else + safe_message.split("\n", 2).first + end end # Returns the commits description @@ -230,6 +231,10 @@ class Commit project.pipelines.where(sha: sha) end + def latest_pipeline + pipelines.last + end + def status(ref = nil) @statuses ||= {} @@ -316,7 +321,14 @@ class Commit end def raw_diffs(*args) - raw.diffs(*args) + use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) + deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only] + + if use_gitaly && !deltas_only + Gitlab::GitalyClient::Commit.diff_from_parent(self, *args) + else + raw.diffs(*args) + end end def diffs(diff_options = nil) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 99a6326309d..8c71267da65 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,15 +5,16 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' - belongs_to :project, foreign_key: :gl_project_id + belongs_to :project belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :user delegate :commit, to: :pipeline + delegate :sha, :short_sha, to: :pipeline validates :pipeline, presence: true, unless: :importing? - validates_presence_of :name + validates :name, presence: true alias_attribute :author, :user @@ -28,9 +29,11 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - # We want to ignore failed_but_allowed jobs + # We want to ignore failed but allowed to fail jobs. + # + # TODO, we also skip ignored optional manual actions. where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]) + false, all_state_names - [:failed, :canceled, :manual]) end scope :retried, -> { where.not(id: latest) } @@ -41,11 +44,11 @@ class CommitStatus < ActiveRecord::Base state_machine :status do event :enqueue do - transition [:created, :skipped] => :pending + transition [:created, :skipped, :manual] => :pending end event :process do - transition skipped: :created + transition [:skipped, :manual] => :created end event :run do @@ -65,7 +68,7 @@ class CommitStatus < ActiveRecord::Base end event :cancel do - transition [:created, :pending, :running] => :canceled + transition [:created, :pending, :running, :manual] => :canceled end before_transition created: [:pending, :running] do |commit_status| @@ -85,7 +88,7 @@ class CommitStatus < ActiveRecord::Base commit_status.run_after_commit do pipeline.try do |pipeline| - if complete? + if complete? || manual? PipelineProcessWorker.perform_async(pipeline.id) else PipelineUpdateWorker.perform_async(pipeline.id) @@ -102,8 +105,6 @@ class CommitStatus < ActiveRecord::Base end end - delegate :sha, :short_sha, to: :pipeline - def before_sha pipeline.before_sha || Gitlab::Git::BLANK_SHA end @@ -132,6 +133,12 @@ class CommitStatus < ActiveRecord::Base false end + # Added in 9.0 to keep backward compatibility for projects exported in 8.17 + # and prior. + def gl_project_id + 'dummy' + end + def detailed_status(current_user) Gitlab::Ci::Status::Factory .new(self, current_user) diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 073ac4c1b65..a7fd0a15f0f 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -101,6 +101,6 @@ module Awardable private def normalize_name(name) - Gitlab::AwardEmoji.normalize_emoji_name(name) + Gitlab::Emoji.normalize_emoji_name(name) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a600f9c14c5..8ea95beed79 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -11,14 +11,15 @@ module CacheMarkdownField # Knows about the relationship between markdown and html field names, and # stores the rendering contexts for the latter class FieldData - extend Forwardable - def initialize @data = {} end - def_delegators :@data, :[], :[]= - def_delegator :@data, :keys, :markdown_fields + delegate :[], :[]=, to: :@data + + def markdown_fields + @data.keys + end def html_field(markdown_field) "#{markdown_field}_html" @@ -45,7 +46,7 @@ module CacheMarkdownField Project Release Snippet - ] + ].freeze def self.caching_classes CACHING_CLASSES.map(&:constantize) diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index fe0cea8465f..034e9f40ff0 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -13,11 +13,12 @@ module CaseSensitivity params.each do |key, value| column = ActiveRecord::Base.connection.quote_table_name(key) - if cast_lower - condition = "LOWER(#{column}) = LOWER(:value)" - else - condition = "#{column} = :value" - end + condition = + if cast_lower + "LOWER(#{column}) = LOWER(:value)" + else + "#{column} = :value" + end criteria = criteria.where(condition, value: value) end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 431c0354969..0a1a65da05a 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -1,23 +1,22 @@ module HasStatus extend ActiveSupport::Concern - DEFAULT_STATUS = 'created' - AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] - STARTED_STATUSES = %w[running success failed skipped] - ACTIVE_STATUSES = %w[pending running] - COMPLETED_STATUSES = %w[success failed canceled skipped] - ORDERED_STATUSES = %w[failed pending running canceled success skipped] + DEFAULT_STATUS = 'created'.freeze + BLOCKED_STATUS = 'manual'.freeze + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze + STARTED_STATUSES = %w[running success failed skipped manual].freeze + ACTIVE_STATUSES = %w[pending running].freeze + COMPLETED_STATUSES = %w[success failed canceled skipped].freeze + ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze class_methods do def status_sql - scope = if respond_to?(:exclude_ignored) - exclude_ignored - else - all - end + scope = respond_to?(:exclude_ignored) ? exclude_ignored : all + builds = scope.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql + manual = scope.manual.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql @@ -30,7 +29,9 @@ module HasStatus WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' - WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' + WHEN (#{running})+(#{pending})>0 THEN 'running' + WHEN (#{manual})>0 THEN 'manual' + WHEN (#{created})>0 THEN 'running' ELSE 'failed' END)" end @@ -63,6 +64,7 @@ module HasStatus state :success, value: 'success' state :canceled, value: 'canceled' state :skipped, value: 'skipped' + state :manual, value: 'manual' end scope :created, -> { where(status: 'created') } @@ -73,12 +75,13 @@ module HasStatus scope :failed, -> { where(status: 'failed') } scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created]) + where(status: [:running, :pending, :created, :manual]) end end @@ -94,6 +97,10 @@ module HasStatus COMPLETED_STATUSES.include?(status) end + def blocked? + BLOCKED_STATUS == status + end + private def calculate_duration diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5f53c48fc88..e7bd20b322a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -16,9 +16,9 @@ module Issuable include TimeTrackable # This object is used to gather issuable meta data for displaying - # upvotes, downvotes and notes count for issues and merge requests + # upvotes, downvotes, notes and closing merge requests count for issues and merge requests # lists avoiding n+1 queries and improving performance. - IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count) + IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count, :merge_requests_count) included do cache_markdown_field :title, pipeline: :single_line @@ -46,12 +46,26 @@ module Issuable has_one :metrics + delegate :name, + :email, + :public_email, + to: :author, + prefix: true + + delegate :name, + :email, + :public_email, + to: :assignee, + allow_nil: true, + prefix: true + validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :recent, -> { reorder(id: :desc) } + scope :order_position_asc, -> { reorder(position: :asc) } scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } @@ -68,21 +82,10 @@ module Issuable scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } - scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) } + scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } - delegate :name, - :email, - to: :author, - prefix: true - - delegate :name, - :email, - to: :assignee, - allow_nil: true, - prefix: true - attr_mentionable :title, pipeline: :single_line attr_mentionable :description @@ -143,7 +146,9 @@ module Issuable when 'milestone_due_desc' then order_milestone_due_desc when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc - when 'priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) + when 'position_asc' then order_position_asc else order_by(method) end @@ -152,7 +157,28 @@ module Issuable sorted.order(id: :desc) end - def order_labels_priority(excluded_labels: []) + def order_due_date_and_labels_priority(excluded_labels: []) + # The order_ methods also modify the query in other ways: + # + # - For milestones, we add a JOIN. + # - For label priority, we change the SELECT, and add a GROUP BY.# + # + # After doing those, we need to reorder to the order we want. The existing + # ORDER BYs won't work because: + # + # 1. We need milestone due date first. + # 2. We can't ORDER BY a column that isn't in the GROUP BY and doesn't + # have an aggregate function applied, so we do a useless MIN() instead. + # + milestones_due_date = 'MIN(milestones.due_date)' + + order_milestone_due_asc. + order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]). + reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'), + Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) + end + + def order_labels_priority(excluded_labels: [], extra_select_columns: []) params = { target_type: name, target_column: "#{table_name}.id", @@ -162,7 +188,12 @@ module Issuable highest_priority = highest_label_priority(params).to_sql - select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). + select_columns = [ + "#{table_name}.*", + "(#{highest_priority}) AS highest_priority" + ] + extra_select_columns + + select(select_columns.join(', ')). group(arel_table[:id]). reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end @@ -182,7 +213,7 @@ module Issuable def grouping_columns(sort) grouping_columns = [arel_table[:id]] - if ["milestone_due_desc", "milestone_due_asc"].include?(sort) + if %w(milestone_due_desc milestone_due_asc).include?(sort) milestone_table = Milestone.arel_table grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:due_date] @@ -232,10 +263,11 @@ module Issuable user: user.hook_attrs, project: project.hook_attrs, object_attributes: hook_attrs, + labels: labels.map(&:hook_attrs), # DEPRECATED repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } - hook_data.merge!(assignee: assignee.hook_attrs) if assignee + hook_data[:assignee] = assignee.hook_attrs if assignee hook_data end diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb index e1f868a299b..713246039c1 100644 --- a/app/models/concerns/reactive_service.rb +++ b/app/models/concerns/reactive_service.rb @@ -5,6 +5,6 @@ module ReactiveService include ReactiveCaching # Default cache key: class name + project_id - self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } end end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb new file mode 100644 index 00000000000..f1d8532a6d6 --- /dev/null +++ b/app/models/concerns/relative_positioning.rb @@ -0,0 +1,139 @@ +module RelativePositioning + extend ActiveSupport::Concern + + MIN_POSITION = 0 + START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 + MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + IDEAL_DISTANCE = 500 + + included do + after_save :save_positionable_neighbours + end + + def max_relative_position + self.class.in_projects(project.id).maximum(:relative_position) + end + + def prev_relative_position + prev_pos = nil + + if self.relative_position + prev_pos = self.class. + in_projects(project.id). + where('relative_position < ?', self.relative_position). + maximum(:relative_position) + end + + prev_pos + end + + def next_relative_position + next_pos = nil + + if self.relative_position + next_pos = self.class. + in_projects(project.id). + where('relative_position > ?', self.relative_position). + minimum(:relative_position) + end + + next_pos + end + + def move_between(before, after) + return move_after(before) unless after + return move_before(after) unless before + + # If there is no place to insert an issue we need to create one by moving the before issue closer + # to its predecessor. This process will recursively move all the predecessors until we have a place + if (after.relative_position - before.relative_position) < 2 + before.move_before + @positionable_neighbours = [before] + end + + self.relative_position = position_between(before.relative_position, after.relative_position) + end + + def move_after(before = self) + pos_before = before.relative_position + pos_after = before.next_relative_position + + if before.shift_after? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after) + issue_to_move.move_after + @positionable_neighbours = [issue_to_move] + + pos_after = issue_to_move.relative_position + end + + self.relative_position = position_between(pos_before, pos_after) + end + + def move_before(after = self) + pos_after = after.relative_position + pos_before = after.prev_relative_position + + if after.shift_before? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before) + issue_to_move.move_before + @positionable_neighbours = [issue_to_move] + + pos_before = issue_to_move.relative_position + end + + self.relative_position = position_between(pos_before, pos_after) + end + + def move_to_end + self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) + end + + # Indicates if there is an issue that should be shifted to free the place + def shift_after? + next_pos = next_relative_position + next_pos && (next_pos - relative_position) == 1 + end + + # Indicates if there is an issue that should be shifted to free the place + def shift_before? + prev_pos = prev_relative_position + prev_pos && (relative_position - prev_pos) == 1 + end + + private + + # This method takes two integer values (positions) and + # calculates the position between them. The range is huge as + # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time + # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number + def position_between(pos_before, pos_after) + pos_before ||= MIN_POSITION + pos_after ||= MAX_POSITION + + pos_before, pos_after = [pos_before, pos_after].sort + + halfway = (pos_after + pos_before) / 2 + distance_to_halfway = pos_after - halfway + + if distance_to_halfway < IDEAL_DISTANCE + halfway + else + if pos_before == MIN_POSITION + pos_after - IDEAL_DISTANCE + elsif pos_after == MAX_POSITION + pos_before + IDEAL_DISTANCE + else + halfway + end + end + end + + def save_positionable_neighbours + return unless @positionable_neighbours + + status = @positionable_neighbours.all?(&:save) + @positionable_neighbours = nil + + status + end +end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 9f6d215ceb3..529fb5ce988 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -51,11 +51,13 @@ module Routable paths.each do |path| path = connection.quote(path) - where = "(routes.path = #{path})" - if cast_lower - where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" - end + where = + if cast_lower + "(LOWER(routes.path) = LOWER(#{path}))" + else + "(routes.path = #{path})" + end wheres << where end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 7edb0acd56c..b9a2d812edd 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -46,11 +46,12 @@ module Sortable where("label_links.target_id = #{target_column}"). reorder(nil) - if target_type_column - query = query.where("label_links.target_type = #{target_type_column}") - else - query = query.where(label_links: { target_type: target_type }) - end + query = + if target_type_column + query.where("label_links.target_type = #{target_type_column}") + else + query.where(label_links: { target_type: target_type }) + end query = query.where.not(title: excluded_labels) if excluded_labels.present? diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb new file mode 100644 index 00000000000..a7fe5951b6e --- /dev/null +++ b/app/models/concerns/uniquify.rb @@ -0,0 +1,30 @@ +class Uniquify + # Return a version of the given 'base' string that is unique + # by appending a counter to it. Uniqueness is determined by + # repeated calls to the passed block. + # + # If `base` is a function/proc, we expect that calling it with a + # candidate counter returns a string to test/return. + def string(base) + @base = base + @counter = nil + + increment_counter! while yield(base_string) + base_string + end + + private + + def base_string + if @base.respond_to?(:call) + @base.call(@counter) + else + "#{@base}#{@counter}" + end + end + + def increment_counter! + @counter ||= 0 + @counter += 1 + end +end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 559b3075905..895a91139c9 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -8,7 +8,7 @@ class DiffNote < Note validates :position, presence: true validates :diff_line, presence: true validates :line_code, presence: true, line_code: true - validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] } + validates :noteable_type, inclusion: { in: %w(Commit MergeRequest) } validates :resolved_by, presence: true, if: :resolved? validate :positions_complete validate :verify_supported diff --git a/app/models/environment.rb b/app/models/environment.rb index 1a21b5e52b5..bf33010fd21 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base project.deployment_service.terminals(self) if has_terminals? end + def has_metrics? + project.monitoring_service.present? && available? && last_deployment.present? + end + + def metrics + project.monitoring_service.metrics(self) if has_metrics? + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/app/models/event.rb b/app/models/event.rb index e5027df3f8a..5c34844b5d3 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -16,7 +16,7 @@ class Event < ActiveRecord::Base RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour - delegate :name, :email, to: :author, prefix: true, allow_nil: true + delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true @@ -36,7 +36,7 @@ class Event < ActiveRecord::Base scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(projects) do - where(project_id: projects).recent + where(project_id: projects.pluck(:id)).recent end scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } @@ -47,7 +47,7 @@ class Event < ActiveRecord::Base def contributions where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)", Event::PUSHED, - ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED], + %w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED], "Note", Event::COMMENTED) end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 26712c19b5a..e63f89a9f85 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -24,6 +24,11 @@ class ExternalIssue def ==(other) other.is_a?(self.class) && (to_s == other.to_s) end + alias_method :eql?, :== + + def hash + [self.class, to_s].hash + end def project @project @@ -43,7 +48,7 @@ class ExternalIssue end def reference_link_text(from_project = nil) - return "##{id}" if /^\d+$/.match(id) + return "##{id}" if id =~ /^\d+$/ id end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index b991d78e27f..0afbca2cb32 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -28,6 +28,28 @@ class GlobalMilestone new(title, child_milestones) end + def self.states_count(projects) + relation = MilestonesFinder.new.execute(projects, state: 'all') + milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count + + opened = count_by_state(milestones_by_state_and_title, 'active') + closed = count_by_state(milestones_by_state_and_title, 'closed') + all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count + + { + opened: opened, + closed: closed, + all: all + } + end + + def self.count_by_state(milestones_by_state_and_title, state) + milestones_by_state_and_title.count do |(milestone_state, _), _| + milestone_state == state + end + end + private_class_method :count_by_state + def initialize(title, milestones) @title = title @name = title diff --git a/app/models/group.rb b/app/models/group.rb index 240a17f1dc1..bd0ecae3da4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -28,6 +28,7 @@ class Group < Namespace validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy after_create :post_create_hook after_destroy :post_destroy_hook @@ -93,7 +94,7 @@ class Group < Namespace end def visibility_level_field - visibility_level + :visibility_level end def visibility_level_allowed_by_projects @@ -212,4 +213,14 @@ class Group < Namespace def users_with_parents User.where(id: members_with_parents.select(:user_id)) end + + def mattermost_team_params + max_length = 59 + + { + name: path[0..max_length], + display_name: name[0..max_length], + type: public? ? 'O' : 'I' # Open vs Invite-only + } + end end diff --git a/app/models/guest.rb b/app/models/guest.rb index 01285ca1264..df287c277a7 100644 --- a/app/models/guest.rb +++ b/app/models/guest.rb @@ -1,6 +1,6 @@ class Guest class << self - def can?(action, subject) + def can?(action, subject = :global) Ability.allowed?(nil, action, subject) end end diff --git a/app/models/issue.rb b/app/models/issue.rb index d8826b65fcc..602eed86d9e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base include Sortable include Spammable include FasterCacheKeys + include RelativePositioning DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -15,8 +16,6 @@ class Issue < ActiveRecord::Base DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze - ActsAsTaggableOn.strict_case_match = true - belongs_to :project belongs_to :moved_to, class_name: 'Issue' @@ -56,10 +55,24 @@ class Issue < ActiveRecord::Base state :opened state :reopened state :closed + + before_transition any => :closed do |issue| + issue.closed_at = Time.zone.now + end + + before_transition closed: any do |issue| + issue.closed_at = nil + end end def hook_attrs - attributes + attrs = { + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate + } + + attributes.merge!(attrs) end def self.reference_prefix @@ -97,6 +110,13 @@ class Issue < ActiveRecord::Base end end + def self.order_by_position_and_priority + order_labels_priority. + reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), + Gitlab::Database.nulls_last_order('highest_priority', 'ASC'), + "id DESC") + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/label.rb b/app/models/label.rb index 5b6b9a7a736..568fa6d44f5 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -11,7 +11,7 @@ class Label < ActiveRecord::Base cache_markdown_field :description, pipeline: :single_line - DEFAULT_COLOR = '#428BCA' + DEFAULT_COLOR = '#428BCA'.freeze default_value_for :color, DEFAULT_COLOR @@ -169,6 +169,10 @@ class Label < ActiveRecord::Base end end + def hook_attrs + attributes + end + private def issues_count(user, params = {}) diff --git a/app/models/member.rb b/app/models/member.rb index d07f270b757..0545bd4eedf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -10,6 +10,8 @@ class Member < ActiveRecord::Base belongs_to :user belongs_to :source, polymorphic: true + delegate :name, :username, :email, to: :user, prefix: true + validates :user, presence: true, unless: :invite? validates :source, presence: true validates :user_id, uniqueness: { scope: [:source_type, :source_id], @@ -73,8 +75,6 @@ class Member < ActiveRecord::Base after_destroy :post_destroy_hook, unless: :pending? after_commit :refresh_member_authorized_projects - delegate :name, :username, :email, to: :user, prefix: true - default_value_for :notification_level, NotificationSetting.levels[:global] class << self diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 204f34f0269..446f9f8f8a7 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,11 +1,11 @@ class GroupMember < Member - SOURCE_TYPE = 'Namespace' + SOURCE_TYPE = 'Namespace'.freeze belongs_to :group, foreign_key: 'source_id' # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE - validates_format_of :source_type, with: /\ANamespace\z/ + validates :source_type, format: { with: /\ANamespace\z/ } default_scope { where(source_type: SOURCE_TYPE) } def self.access_level_roles diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 008fff0857c..912820b51ac 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,5 +1,5 @@ class ProjectMember < Member - SOURCE_TYPE = 'Project' + SOURCE_TYPE = 'Project'.freeze include Gitlab::ShellAdapter @@ -7,7 +7,7 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE - validates_format_of :source_type, with: /\AProject\z/ + validates :source_type, format: { with: /\AProject\z/ } validates :access_level, inclusion: { in: Gitlab::Access.values } default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 204d2b153ad..cef8ad76b07 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" + belongs_to :project, foreign_key: :target_project_id belongs_to :merge_user, class_name: "User" has_many :merge_request_diffs, dependent: :destroy @@ -91,17 +92,13 @@ class MergeRequest < ActiveRecord::Base around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end - - after_transition unchecked: :cannot_be_merged do |merge_request, transition| - TodoService.new.merge_request_became_unmergeable(merge_request) - end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true - validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing? + validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? @@ -203,7 +200,11 @@ class MergeRequest < ActiveRecord::Base end def diff_size - opts = diff_options || {} + # The `#diffs` method ends up at an instance of a class inheriting from + # `Gitlab::Diff::FileCollection::Base`, so use those options as defaults + # here too, to get the same diff size without performing highlighting. + # + opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {}) raw_diffs(opts).size end @@ -436,7 +437,7 @@ class MergeRequest < ActiveRecord::Base true end - def can_cancel_merge_when_build_succeeds?(current_user) + def can_cancel_merge_when_pipeline_succeeds?(current_user) can_be_merged_by?(current_user) || self.author == current_user end @@ -523,11 +524,14 @@ class MergeRequest < ActiveRecord::Base source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, - work_in_progress: work_in_progress? + work_in_progress: work_in_progress?, + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate } if diff_head_commit - attrs.merge!(last_commit: diff_head_commit.hook_attrs) + attrs[:last_commit] = diff_head_commit.hook_attrs end attributes.merge!(attrs) @@ -537,10 +541,6 @@ class MergeRequest < ActiveRecord::Base target_project != source_project end - def project - target_project - end - # If the merge request closes any issues, save this information in the # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires @@ -644,10 +644,10 @@ class MergeRequest < ActiveRecord::Base message.join("\n\n") end - def reset_merge_when_build_succeeds - return unless merge_when_build_succeeds? + def reset_merge_when_pipeline_succeeds + return unless merge_when_pipeline_succeeds? - self.merge_when_build_succeeds = false + self.merge_when_pipeline_succeeds = false self.merge_user = nil if merge_params merge_params.delete('should_remove_source_branch') @@ -684,7 +684,10 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - source_project.try(:ci_service) && commits.any? + has_ci_integration = source_project.try(:ci_service) + uses_gitlab_ci = all_pipelines.any? + + (has_ci_integration || uses_gitlab_ci) && commits.any? end def branch_missing? @@ -706,7 +709,7 @@ class MergeRequest < ActiveRecord::Base end def mergeable_ci_state? - return true unless project.only_allow_merge_if_build_succeeds? + return true unless project.only_allow_merge_if_pipeline_succeeds? !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 70bad2a4396..baee00b8fcd 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base COMMITS_SAFE_SIZE = 100 # Valid types of serialized diffs allowed by Gitlab::Git::Diff - VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta] + VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze belongs_to :merge_request diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb index ab597c37947..daafb137be4 100644 --- a/app/models/merge_requests_closing_issues.rb +++ b/app/models/merge_requests_closing_issues.rb @@ -4,4 +4,12 @@ class MergeRequestsClosingIssues < ActiveRecord::Base validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true validates :issue_id, presence: true + + class << self + def count_for_collection(ids) + group(:issue_id). + where(issue_id: ids). + pluck('issue_id', 'COUNT(*) as count') + end + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7331000a9f2..c0deb59ec4c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) Any = MilestoneStruct.new('Any Milestone', '', -1) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) + Started = MilestoneStruct.new('Started', '#started', -3) include CacheMarkdownField include InternalId diff --git a/app/models/namespace.rb b/app/models/namespace.rb index a803be2e780..4ae9d0122f2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -20,6 +20,7 @@ class Namespace < ActiveRecord::Base belongs_to :parent, class_name: "Namespace" has_many :children, class_name: "Namespace", foreign_key: :parent_id + has_one :chat_team, dependent: :destroy validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, @@ -98,14 +99,8 @@ class Namespace < ActiveRecord::Base # Work around that by setting their username to "blank", followed by a counter. path = "blank" if path.blank? - counter = 0 - base = path - while Namespace.find_by_path_or_name(path) - counter += 1 - path = "#{base}#{counter}" - end - - path + uniquify = Uniquify.new + uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) } end end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index b524ca50ee8..0bbc9451ffd 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -188,11 +188,12 @@ module Network end # and mark it as reserved - if parent_time.nil? - min_time = leaves.first.time - else - min_time = parent_time + 1 - end + min_time = + if parent_time.nil? + leaves.first.time + else + parent_time + 1 + end max_time = leaves.last.time leaves.last.parents(@map).each do |parent| diff --git a/app/models/note.rb b/app/models/note.rb index 029fe667a45..e22e96aec6f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -72,7 +72,7 @@ class Note < ActiveRecord::Base scope :inc_author, ->{ includes(:author) } scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) } - scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) } + scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } scope :with_associations, -> do @@ -85,6 +85,7 @@ class Note < ActiveRecord::Base before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :set_discussion_id after_save :keep_around_commit, unless: :for_personal_snippet? + after_save :expire_etag_cache class << self def model_name @@ -231,10 +232,6 @@ class Note < ActiveRecord::Base note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/ end - def award_emoji_name - note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - end - def to_ability_name for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore end @@ -276,4 +273,16 @@ class Note < ActiveRecord::Base self.class.build_discussion_id(noteable_type, noteable_id || commit_id) end end + + def expire_etag_cache + return unless for_issue? + + key = Gitlab::Routing.url_helpers.namespace_project_noteable_notes_path( + noteable.project.namespace, + noteable.project, + target_type: noteable_type.underscore, + target_id: noteable.id + ) + Gitlab::EtagCaching::Store.new.touch(key) + end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 58f6214bea7..52577bd52ea 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -35,11 +35,11 @@ class NotificationSetting < ActiveRecord::Base :merge_merge_request, :failed_pipeline, :success_pipeline - ] + ].freeze EXCLUDED_WATCHER_EVENTS = [ :success_pipeline - ] + ].freeze store :events, accessors: EMAIL_EVENTS, coder: JSON diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb new file mode 100644 index 00000000000..3a997406565 --- /dev/null +++ b/app/models/oauth_access_grant.rb @@ -0,0 +1,4 @@ +class OauthAccessGrant < Doorkeeper::AccessGrant + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index 116fb71ac08..b85f5dbaf2e 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,4 +1,4 @@ -class OauthAccessToken < ActiveRecord::Base +class OauthAccessToken < Doorkeeper::AccessToken belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 0b9ebf1ffe2..f2f2fc1e32a 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -2,7 +2,7 @@ class PagesDomain < ActiveRecord::Base belongs_to :project validates :domain, hostname: true - validates_uniqueness_of :domain, case_sensitive: false + validates :domain, uniqueness: { case_sensitive: false } validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 10a34c42fd8..e8b000ddad6 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -1,4 +1,5 @@ class PersonalAccessToken < ActiveRecord::Base + include Expirable include TokenAuthenticatable add_authentication_token_field :token @@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base belongs_to :user - scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") } + before_save :ensure_token + + scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") } scope :inactive, -> { where("revoked = true OR expires_at < NOW()") } + scope :with_impersonation, -> { where(impersonation: true) } + scope :without_impersonation, -> { where(impersonation: false) } - def self.generate(params) - personal_access_token = self.new(params) - personal_access_token.ensure_token - personal_access_token - end + validates :scopes, presence: true + validate :validate_api_scopes def revoke! self.revoked = true self.save end + + def active? + !revoked? && !expired? + end + + protected + + def validate_api_scopes + unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) } + errors.add :scopes, "can only contain API scopes" + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index d4f5584f53d..928965643a0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,10 +19,10 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper - class BoardLimitExceeded < StandardError; end + BoardLimitExceeded = Class.new(StandardError) NUMBER_OF_PERMITTED_BOARDS = 1 - UNKNOWN_IMPORT_URL = 'http://unknown.git' + UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze cache_markdown_field :description, pipeline: :description @@ -70,8 +70,7 @@ class Project < ActiveRecord::Base after_validation :check_pending_delete - ActsAsTaggableOn.strict_case_match = true - acts_as_taggable_on :tags + acts_as_taggable attr_accessor :new_default_branch attr_accessor :old_path_with_namespace @@ -90,7 +89,6 @@ class Project < ActiveRecord::Base has_one :campfire_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy - has_one :builds_email_service, dependent: :destroy has_one :pipelines_email_service, dependent: :destroy has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy @@ -114,6 +112,8 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy has_one :kubernetes_service, dependent: :destroy, inverse_of: :project + has_one :prometheus_service, dependent: :destroy, inverse_of: :project + has_one :mock_ci_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -159,13 +159,13 @@ class Project < ActiveRecord::Base has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_many :container_images, dependent: :destroy - has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id - has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id - has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses - has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id + has_many :commit_statuses, dependent: :destroy + has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline' + has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' - has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id - has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' has_many :environments, dependent: :destroy has_many :deployments, dependent: :destroy @@ -173,9 +173,11 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :project_feature delegate :name, to: :owner, allow_nil: true, prefix: true + delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team + delegate :empty_repo?, to: :repository # Validations validates :creator, presence: true, on: :create @@ -192,9 +194,10 @@ class Project < ActiveRecord::Base format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } validates :namespace, presence: true - validates_uniqueness_of :name, scope: :namespace_id - validates_uniqueness_of :path, scope: :namespace_id + validates :name, uniqueness: { scope: :namespace_id } + validates :path, uniqueness: { scope: :namespace_id } validates :import_url, addressable_url: true, if: :external_import? + validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, @@ -211,6 +214,7 @@ class Project < ActiveRecord::Base before_save :ensure_runners_token mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy # Scopes default_scope { where(pending_delete: false) } @@ -334,7 +338,7 @@ class Project < ActiveRecord::Base end def search_by_visibility(level) - where(visibility_level: Gitlab::VisibilityLevel.const_get(level.upcase)) + where(visibility_level: Gitlab::VisibilityLevel.string_options[level]) end def search_by_title(query) @@ -359,7 +363,7 @@ class Project < ActiveRecord::Base end def reference_pattern - name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR + name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR %r{ ((?<namespace>#{name_pattern})\/)? @@ -390,7 +394,7 @@ class Project < ActiveRecord::Base end def repository_storage_path - Gitlab.config.repositories.storages[repository_storage] + Gitlab.config.repositories.storages[repository_storage]['path'] end def team @@ -452,13 +456,14 @@ class Project < ActiveRecord::Base end def add_import_job - if forked? - job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, - forked_from_project.path_with_namespace, - self.namespace.full_path) - else - job_id = RepositoryImportWorker.perform_async(self.id) - end + job_id = + if forked? + RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, + self.namespace.full_path) + else + RepositoryImportWorker.perform_async(self.id) + end if job_id Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" @@ -766,6 +771,14 @@ class Project < ActiveRecord::Base @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) end + def monitoring_services + services.where(category: :monitoring) + end + + def monitoring_service + @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end @@ -836,10 +849,6 @@ class Project < ActiveRecord::Base false end - def empty_repo? - repository.empty_repo? - end - def repo repository.raw end @@ -848,10 +857,6 @@ class Project < ActiveRecord::Base gitlab_shell.url_to_repo(path_with_namespace) end - def namespace_dir - namespace.try(:path) || '' - end - def repo_exists? @repo_exists ||= repository.exists? rescue @@ -874,8 +879,10 @@ class Project < ActiveRecord::Base url_to_repo end - def http_url_to_repo - "#{web_url}.git" + def http_url_to_repo(user = nil) + credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) + + Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url end # Check if current branch name is marked as protected in the system @@ -900,8 +907,8 @@ class Project < ActiveRecord::Base def rename_repo path_was = previous_changes['path'].first - old_path_with_namespace = File.join(namespace_dir, path_was) - new_path_with_namespace = File.join(namespace_dir, path) + old_path_with_namespace = File.join(namespace.full_path, path_was) + new_path_with_namespace = File.join(namespace.full_path, path) Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" @@ -1002,7 +1009,7 @@ class Project < ActiveRecord::Base end def visibility_level_field - visibility_level + :visibility_level end def archive! @@ -1027,10 +1034,6 @@ class Project < ActiveRecord::Base forked? && project == forked_from_project end - def forks_count - forks.count - end - def origin_merge_requests merge_requests.where(source_project_id: self.id) end @@ -1201,6 +1204,10 @@ class Project < ActiveRecord::Base end end + def pipeline_status + @pipeline_status ||= Ci::PipelineStatus.load_for_project(self) + end + def mark_import_as_failed(error_message) original_errors = errors.dup sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 03194fc2141..e3ef4919b28 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -18,7 +18,7 @@ class ProjectFeature < ActiveRecord::Base PRIVATE = 10 ENABLED = 20 - FEATURES = %i(issues merge_requests wiki snippets builds repository) + FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze class << self def access_level_attribute(feature) diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 5cb6b0c527d..ac1e9ab2b0b 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base private def different_group - if self.group && self.project && self.project.group == self.group - errors.add(:base, "Project cannot be shared with the project it is in.") + return unless self.group && self.project + + project_group = self.project.group + return unless project_group + + group_ids = project_group.ancestors.map(&:id).push(project_group.id) + + if group_ids.include?(self.group.id) + errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.") end end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 0956c4a4ede..5fb95050b83 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -3,7 +3,7 @@ require "addressable/uri" class BuildkiteService < CiService include ReactiveService - ENDPOINT = "https://buildkite.com" + ENDPOINT = "https://buildkite.com".freeze prop_accessor :project_url, :token boolean_accessor :enable_ssl_verification diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index ebd21e37189..0c526b53d72 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,107 +1,11 @@ +# This class is to be removed with 9.1 +# We should also by then remove BuildsEmailService from database class BuildsEmailService < Service - prop_accessor :recipients - boolean_accessor :add_pusher - boolean_accessor :notify_only_broken_builds - validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? } - - def initialize_properties - if properties.nil? - self.properties = {} - self.notify_only_broken_builds = true - end - end - - def title - 'Builds emails' - end - - def description - 'Email the builds status to a list of recipients.' - end - def self.to_param 'builds_email' end def self.supported_events - %w(build) - end - - def execute(push_data) - return unless supported_events.include?(push_data[:object_kind]) - return unless should_build_be_notified?(push_data) - - recipients = all_recipients(push_data) - - if recipients.any? - BuildEmailWorker.perform_async( - push_data[:build_id], - recipients, - push_data - ) - end - end - - def can_test? - project.builds.any? - end - - def disabled_title - "Please setup a build on your repository." - end - - def test_data(project = nil, user = nil) - Gitlab::DataBuilder::Build.build(project.builds.last) - end - - def fields - [ - { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' }, - { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, - ] - end - - def test(data) - begin - # bypass build status verification when testing - data[:build_status] = "failed" - data[:build_allow_failure] = false - - result = execute(data) - rescue StandardError => error - return { success: false, result: error } - end - - { success: true, result: result } - end - - def should_build_be_notified?(data) - case data[:build_status] - when 'success' - !notify_only_broken_builds? - when 'failed' - !allow_failure?(data) - else - false - end - end - - def allow_failure?(data) - data[:build_allow_failure] == true - end - - def all_recipients(data) - all_recipients = [] - - unless recipients.blank? - all_recipients += recipients.split(',').compact.reject(&:blank?) - end - - if add_pusher? && data[:user][:email] - all_recipients << data[:user][:email] - end - - all_recipients + %w[] end end diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb deleted file mode 100644 index c776e0a20c4..00000000000 --- a/app/models/project_services/chat_message/build_message.rb +++ /dev/null @@ -1,102 +0,0 @@ -module ChatMessage - class BuildMessage < BaseMessage - attr_reader :sha - attr_reader :ref_type - attr_reader :ref - attr_reader :status - attr_reader :project_name - attr_reader :project_url - attr_reader :user_name - attr_reader :user_url - attr_reader :duration - attr_reader :stage - attr_reader :build_id - attr_reader :build_name - - def initialize(params) - @sha = params[:sha] - @ref_type = params[:tag] ? 'tag' : 'branch' - @ref = params[:ref] - @project_name = params[:project_name] - @project_url = params[:project_url] - @status = params[:commit][:status] - @user_name = params[:commit][:author_name] - @user_url = params[:commit][:author_url] - @duration = params[:commit][:duration] - @stage = params[:build_stage] - @build_name = params[:build_name] - @build_id = params[:build_id] - end - - def pretext - '' - end - - def fallback - format(message) - end - - def attachments - [{ text: format(message), color: attachment_color }] - end - - private - - def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}" - end - - def build_url - "#{project_url}/builds/#{build_id}" - end - - def build_link - link(build_name, build_url) - end - - def user_link - link(user_name, user_url) - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def humanized_status - case status - when 'success' - 'passed' - else - status - end - end - - def attachment_color - if status == 'success' - 'good' - else - 'danger' - end - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def branch_link - link(ref, branch_url) - end - - def project_link - link(project_name, project_url) - end - - def commit_url - "#{project_url}/commit/#{sha}/builds" - end - - def commit_link - link(Commit.truncate_sha(sha), commit_url) - end - end -end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index b96aca47e65..791e5b0cec7 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -51,7 +51,8 @@ module ChatMessage title: issue_title, title_link: issue_url, text: format(description), - color: "#C95823" }] + color: "#C95823" + }] end def project_link diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 8468934425f..200be99f36b 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -6,7 +6,7 @@ class ChatNotificationService < Service default_value_for :category, 'chat' prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines + boolean_accessor :notify_only_broken_pipelines validates :webhook, presence: true, url: true, if: :activated? @@ -16,7 +16,6 @@ class ChatNotificationService < Service if properties.nil? self.properties = {} - self.notify_only_broken_builds = true self.notify_only_broken_pipelines = true end end @@ -27,7 +26,7 @@ class ChatNotificationService < Service def self.supported_events %w[push issue confidential_issue merge_request note tag_push - build pipeline wiki_page] + pipeline wiki_page] end def execute(data) @@ -89,8 +88,6 @@ class ChatNotificationService < Service ChatMessage::MergeMessage.new(data) unless is_update?(data) when "note" ChatMessage::NoteMessage.new(data) - when "build" - ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data) when "pipeline" ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) when "wiki_page" @@ -125,17 +122,6 @@ class ChatNotificationService < Service data[:object_attributes][:action] == 'update' end - def should_build_be_notified?(data) - case data[:commit][:status] - when 'success' - !notify_only_broken_builds? - when 'failed' - true - else - false - end - end - def should_pipeline_be_notified?(data) case data[:object_attributes][:status] when 'success' diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 1ad9efac196..2717c240f05 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -39,7 +39,7 @@ class DroneCiService < CiService def commit_status_path(sha, ref) url = [drone_url, "gitlab/#{project.full_path}/commits/#{sha}", - "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"] + "?branch=#{URI.encode(ref.to_s)}&access_token=#{token}"] URI.join(*url).to_s end @@ -74,7 +74,7 @@ class DroneCiService < CiService def build_page(sha, ref) url = [drone_url, "gitlab/#{project.full_path}/redirect/commits/#{sha}", - "?branch=#{URI::encode(ref.to_s)}"] + "?branch=#{URI.encode(ref.to_s)}"] URI.join(*url).to_s end @@ -114,7 +114,7 @@ class DroneCiService < CiService end def merge_request_valid?(data) - ['opened', 'reopened'].include?(data[:object_attributes][:state]) && + %w(opened reopened).include?(data[:object_attributes][:state]) && data[:object_attributes][:merge_status] == 'unchecked' end end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 72da219df28..8b181221bb0 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -6,16 +6,16 @@ class HipchatService < Service a b i strong em br img pre code table th tr td caption colgroup col thead tbody tfoot ul ol li dl dt dd - ] + ].freeze prop_accessor :token, :room, :server, :color, :api_version - boolean_accessor :notify_only_broken_builds, :notify + boolean_accessor :notify_only_broken_pipelines, :notify validates :token, presence: true, if: :activated? def initialize_properties if properties.nil? self.properties = {} - self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true end end @@ -36,17 +36,17 @@ class HipchatService < Service { type: 'text', name: 'token', placeholder: 'Room token' }, { type: 'text', name: 'room', placeholder: 'Room name or ID' }, { type: 'checkbox', name: 'notify' }, - { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] }, + { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) }, { type: 'text', name: 'api_version', placeholder: 'Leave blank for default (v2)' }, { type: 'text', name: 'server', placeholder: 'Leave blank for default. https://hipchat.example.com' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def self.supported_events - %w(push issue confidential_issue merge_request note tag_push build) + %w(push issue confidential_issue merge_request note tag_push pipeline) end def execute(data) @@ -90,8 +90,8 @@ class HipchatService < Service create_merge_request_message(data) unless is_update?(data) when "note" create_note_message(data) - when "build" - create_build_message(data) if should_build_be_notified?(data) + when "pipeline" + create_pipeline_message(data) if should_pipeline_be_notified?(data) end end @@ -240,28 +240,29 @@ class HipchatService < Service message end - def create_build_message(data) - ref_type = data[:tag] ? 'tag' : 'branch' - ref = data[:ref] - sha = data[:sha] - user_name = data[:commit][:author_name] - status = data[:commit][:status] - duration = data[:commit][:duration] + def create_pipeline_message(data) + pipeline_attributes = data[:object_attributes] + pipeline_id = pipeline_attributes[:id] + ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + ref = pipeline_attributes[:ref] + user_name = (data[:user] && data[:user][:name]) || 'API' + status = pipeline_attributes[:status] + duration = pipeline_attributes[:duration] branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>" - commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>" + pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>" - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" + "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" end def message_color(data) - build_status_color(data) || color || 'yellow' + pipeline_status_color(data) || color || 'yellow' end - def build_status_color(data) - return unless data && data[:object_kind] == 'build' + def pipeline_status_color(data) + return unless data && data[:object_kind] == 'pipeline' - case data[:commit][:status] + case data[:object_attributes][:status] when 'success' 'green' else @@ -294,10 +295,10 @@ class HipchatService < Service end end - def should_build_be_notified?(data) - case data[:commit][:status] + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] when 'success' - !notify_only_broken_builds? + !notify_only_broken_pipelines? when 'failed' true else diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 5d6862d9faa..c62bb4fa120 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -33,7 +33,8 @@ class IrkerService < Service end def settings - { server_host: server_host.present? ? server_host : 'localhost', + { + server_host: server_host.present? ? server_host : 'localhost', server_port: server_port.present? ? server_port : 6659 } end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 9e65fdbf9d6..50435b67eda 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,4 +1,6 @@ class IssueTrackerService < Service + validate :one_issue_tracker, if: :activated?, on: :manual_change + default_value_for :category, 'issue_tracker' # Pattern used to extract links from comments @@ -92,4 +94,13 @@ class IssueTrackerService < Service def issues_tracker Gitlab.config.issues_tracker[to_param] end + + def one_issue_tracker + return if template? + return if project.blank? + + if project.services.external_issue_trackers.where.not(id: id).any? + errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time') + end + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f2f019c43bb..02fbd5497fa 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -3,7 +3,7 @@ class KubernetesService < DeploymentService include Gitlab::Kubernetes include ReactiveCaching - self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } # Namespace defaults to the project path, but can be overridden in case that # is an invalid or inappropriate name @@ -36,7 +36,7 @@ class KubernetesService < DeploymentService def initialize_properties if properties.nil? self.properties = {} - self.namespace = project.path if project.present? + self.namespace = "#{project.path}-#{project.id}" if project.present? end end @@ -62,23 +62,19 @@ class KubernetesService < DeploymentService { type: 'text', name: 'namespace', title: 'Kubernetes namespace', - placeholder: 'Kubernetes namespace', - }, + placeholder: 'Kubernetes namespace' }, { type: 'text', name: 'api_url', title: 'API URL', - placeholder: 'Kubernetes API URL, like https://kube.example.com/', - }, + placeholder: 'Kubernetes API URL, like https://kube.example.com/' }, { type: 'text', name: 'token', title: 'Service token', - placeholder: 'Service token', - }, + placeholder: 'Service token' }, { type: 'textarea', name: 'ca_pem', title: 'Custom CA bundle', - placeholder: 'Certificate Authority bundle (PEM format)', - }, + placeholder: 'Certificate Authority bundle (PEM format)' }, ] end @@ -98,7 +94,12 @@ class KubernetesService < DeploymentService { key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_NAMESPACE', value: namespace, public: true } ] - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present? + + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + end + variables end @@ -167,7 +168,7 @@ class KubernetesService < DeploymentService url = URI.parse(api_url) prefix = url.path.sub(%r{/+\z}, '') - url.path = [ prefix, *parts ].join("/") + url.path = [prefix, *parts].join("/") url.to_s end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 4ebc5318da1..1156d050622 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService 'This service sends notifications about projects events to Mattermost channels.<br /> To set up this service: <ol> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> - <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li> + <li>Paste the webhook <strong>URL</strong> into the field below.</li> + <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li> </ol>' end @@ -28,14 +28,13 @@ class MattermostService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "town-square" + "Channel handle (e.g. town-square)" end end diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb new file mode 100644 index 00000000000..a8d581a1f67 --- /dev/null +++ b/app/models/project_services/mock_ci_service.rb @@ -0,0 +1,82 @@ +# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service +class MockCiService < CiService + ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze + + prop_accessor :mock_service_url + validates :mock_service_url, presence: true, url: true, if: :activated? + + def title + 'MockCI' + end + + def description + 'Mock an external CI' + end + + def self.to_param + 'mock_ci' + end + + def fields + [ + { type: 'text', + name: 'mock_service_url', + placeholder: 'http://localhost:4004' }, + ] + end + + # Return complete url to build page + # + # Ex. + # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c + # + def build_page(sha, ref) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}"] + + URI.join(*url).to_s + end + + # Return string with build status or :error symbol + # + # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' + # + # + # Ex. + # @service.commit_status('13be4ac', 'master') + # # => 'success' + # + # @service.commit_status('2abe4ac', 'dev') + # # => 'running' + # + # + def commit_status(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + read_commit_status(response) + rescue Errno::ECONNREFUSED + :error + end + + def commit_status_path(sha) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}.json"] + + URI.join(*url).to_s + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'pending' + else + response['status'] + end + + if status.present? && ALLOWED_STATES.include?(status) + status + else + :error + end + end +end diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb new file mode 100644 index 00000000000..ea585721e8f --- /dev/null +++ b/app/models/project_services/monitoring_service.rb @@ -0,0 +1,16 @@ +# Base class for monitoring services +# +# These services integrate with a deployment solution like Prometheus +# to provide additional features for environments. +class MonitoringService < Service + default_value_for :category, 'monitoring' + + def self.supported_events + %w() + end + + # Environments have a number of metrics + def metrics(environment) + raise NotImplementedError + end +end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 9cc642591f4..d86f4f6f448 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,7 +1,7 @@ class PivotaltrackerService < Service include HTTParty - API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits' + API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze prop_accessor :token, :restrict_to_branch validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb new file mode 100644 index 00000000000..375966b9efc --- /dev/null +++ b/app/models/project_services/prometheus_service.rb @@ -0,0 +1,93 @@ +class PrometheusService < MonitoringService + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + self.reactive_cache_lease_timeout = 30.seconds + self.reactive_cache_refresh_interval = 30.seconds + self.reactive_cache_lifetime = 1.minute + + # Access to prometheus is directly through the API + prop_accessor :api_url + + with_options presence: true, if: :activated? do + validates :api_url, url: true + end + + after_save :clear_reactive_cache! + + def initialize_properties + if properties.nil? + self.properties = {} + end + end + + def title + 'Prometheus' + end + + def description + 'Prometheus monitoring' + end + + def help + 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' + end + + def self.to_param + 'prometheus' + end + + def fields + [ + { + type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ] + end + + # Check we can connect to the Prometheus API + def test(*args) + client.ping + + { success: true, result: 'Checked API endpoint' } + rescue Gitlab::PrometheusError => err + { success: false, result: err } + end + + def metrics(environment) + with_reactive_cache(environment.slug) do |data| + data + end + end + + # Cache metrics for specific environment + def calculate_reactive_cache(environment_slug) + return unless active? && project && !project.pending_delete? + + memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + + { + success: true, + metrics: { + # Memory used in MB + memory_values: client.query_range(memory_query, start: 8.hours.ago), + memory_current: client.query(memory_query), + # CPU Usage rate in cores. + cpu_values: client.query_range(cpu_query, start: 8.hours.ago), + cpu_current: client.query(cpu_query) + }, + last_update: Time.now.utc + } + + rescue Gitlab::PrometheusError => err + { success: false, result: err.message } + end + + def client + @prometheus ||= Gitlab::Prometheus.new(api_url: api_url) + end +end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index a963d27a376..3e618a8dbf1 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -29,25 +29,24 @@ class PushoverService < Service ['Normal Priority', 0], ['High Priority', 1] ], - default_choice: 0 - }, + default_choice: 0 }, { type: 'select', name: 'sound', choices: [ ['Device default sound', nil], ['Pushover (default)', 'pushover'], - ['Bike', 'bike'], - ['Bugle', 'bugle'], + %w(Bike bike), + %w(Bugle bugle), ['Cash Register', 'cashregister'], - ['Classical', 'classical'], - ['Cosmic', 'cosmic'], - ['Falling', 'falling'], - ['Gamelan', 'gamelan'], - ['Incoming', 'incoming'], - ['Intermission', 'intermission'], - ['Magic', 'magic'], - ['Mechanical', 'mechanical'], + %w(Classical classical), + %w(Cosmic cosmic), + %w(Falling falling), + %w(Gamelan gamelan), + %w(Incoming incoming), + %w(Intermission intermission), + %w(Magic magic), + %w(Mechanical mechanical), ['Piano Bar', 'pianobar'], - ['Siren', 'siren'], + %w(Siren siren), ['Space Alarm', 'spacealarm'], ['Tug Boat', 'tugboat'], ['Alien Alarm (long)', 'alien'], @@ -56,8 +55,7 @@ class PushoverService < Service ['Pushover Echo (long)', 'echo'], ['Up Down (long)', 'updown'], ['None (silent)', 'none'] - ] - }, + ] }, ] end @@ -72,13 +70,14 @@ class PushoverService < Service before = data[:before] after = data[:after] - if Gitlab::Git.blank_ref?(before) - message = "#{data[:user_name]} pushed new branch \"#{ref}\"." - elsif Gitlab::Git.blank_ref?(after) - message = "#{data[:user_name]} deleted branch \"#{ref}\"." - else - message = "#{data[:user_name]} push to branch \"#{ref}\"." - end + message = + if Gitlab::Git.blank_ref?(before) + "#{data[:user_name]} pushed new branch \"#{ref}\"." + elsif Gitlab::Git.blank_ref?(after) + "#{data[:user_name]} deleted branch \"#{ref}\"." + else + "#{data[:user_name]} push to branch \"#{ref}\"." + end if data[:total_commits_count] > 0 message << "\nTotal commits count: #{data[:total_commits_count]}" @@ -97,7 +96,7 @@ class PushoverService < Service # Sound parameter MUST NOT be sent to API if not selected if sound - pushover_data.merge!(sound: sound) + pushover_data[:sound] = sound end PushoverService.post('/messages.json', body: pushover_data) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index f77d2d7c60b..b657db6f9ee 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -13,11 +13,11 @@ class SlackService < ChatNotificationService def help 'This service sends notifications about projects events to Slack channels.<br /> - To setup this service: + To set up this service: <ol> - <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> - <li>Paste the <strong>Webhook URL</strong> into the field below. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li> + <li>Paste the <strong>Webhook URL</strong> into the field below.</li> + <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li> </ol>' end @@ -27,14 +27,13 @@ class SlackService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "#general" + "Channel name (e.g. general)" end end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 06abd406523..aeaf63abab9 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -4,7 +4,7 @@ class ProjectStatistics < ActiveRecord::Base before_save :update_storage_size - STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size] + STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS def total_repository_size diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index d0b991db112..70eef359cdd 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -5,9 +5,9 @@ class ProjectWiki 'Markdown' => :markdown, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc - } unless defined?(MARKUPS) + }.freeze unless defined?(MARKUPS) - class CouldNotCreateWikiError < StandardError; end + CouldNotCreateWikiError = Class.new(StandardError) # Returns a string describing what went wrong after # an operation fails. @@ -19,6 +19,9 @@ class ProjectWiki @user = user end + delegate :empty?, to: :pages + delegate :repository_storage_path, to: :project + def path @project.path + '.wiki' end @@ -39,8 +42,11 @@ class ProjectWiki url_to_repo end - def http_url_to_repo - [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') + def http_url_to_repo(user = nil) + url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git" + credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) + + Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url end def wiki_base_path @@ -60,10 +66,6 @@ class ProjectWiki !!repository.exists? end - def empty? - pages.empty? - end - # Returns an Array of Gitlab WikiPage instances or an # empty Array if this Wiki has no pages. def pages @@ -160,10 +162,6 @@ class ProjectWiki } end - def repository_storage_path - project.repository_storage_path - end - private def init_repo(path_with_namespace) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 6240912a6e1..39e979ef15b 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,8 +8,8 @@ class ProtectedBranch < ActiveRecord::Base has_many :merge_access_levels, dependent: :destroy has_many :push_access_levels, dependent: :destroy - validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch." - validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch." + validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." } + validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." } accepts_nested_attributes_for :push_access_levels accepts_nested_attributes_for :merge_access_levels diff --git a/app/models/repository.rb b/app/models/repository.rb index 56c582cd9be..6ab04440ca8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,6 +6,7 @@ class Repository attr_accessor :path_with_namespace, :project CommitError = Class.new(StandardError) + CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -18,7 +19,7 @@ class Repository CACHED_METHODS = %i(size commit_count readme version contribution_guide changelog license_blob license_key gitignore koding_yml gitlab_ci_yml branch_names tag_names branch_count - tag_count avatar exists? empty? root_ref) + tag_count avatar exists? empty? root_ref).freeze # Certain method caches should be refreshed when certain types of files are # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to @@ -33,7 +34,7 @@ class Repository koding: :koding_yml, gitlab_ci: :gitlab_ci_yml, avatar: :avatar - } + }.freeze # Wraps around the given method and caches its output in Redis and an instance # variable. @@ -49,10 +50,6 @@ class Repository end end - def self.storages - Gitlab.config.repositories.storages - end - def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project @@ -109,9 +106,7 @@ class Repository offset: offset, after: after, before: before, - # --follow doesn't play well with --skip. See: - # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 - follow: false, + follow: path.present?, skip_merges: skip_merges } @@ -317,11 +312,13 @@ class Repository if !branch_name || branch_name == root_ref branches.each do |branch| cache.expire(:"diverging_commit_counts_#{branch.name}") + cache.expire(:"commit_count_#{branch.name}") end # In case a commit is pushed to a non-root branch we only have to flush the # cache for said branch. else cache.expire(:"diverging_commit_counts_#{branch_name}") + cache.expire(:"commit_count_#{branch_name}") end end @@ -487,9 +484,7 @@ class Repository end cache_method :exists? - def empty? - raw_repository.empty? - end + delegate :empty?, to: :raw_repository cache_method :empty? # The size of this repository in megabytes. @@ -503,14 +498,22 @@ class Repository end cache_method :commit_count, fallback: 0 + def commit_count_for_ref(ref) + return 0 unless exists? + + begin + cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) } + rescue Rugged::ReferenceError + 0 + end + end + def branch_names branches.map(&:name) end cache_method :branch_names, fallback: [] - def tag_names - raw_repository.tag_names - end + delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] def branch_count @@ -750,136 +753,63 @@ class Repository @tags ||= raw_repository.tags end - # rubocop:disable Metrics/ParameterLists - def commit_dir( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - check_tree_entry_for_dir(branch_name, path) - - if start_branch_name - start_project.repository. - check_tree_entry_for_dir(start_branch_name, path) - end + def create_dir(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :create_dir, file_path: path }] - commit_file( - user, - "#{path}/.gitkeep", - '', - message: message, - branch_name: branch_name, - update: false, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def commit_file( - user, path, content, - message:, branch_name:, update: true, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - unless update - error_message = "Filename already exists; update not allowed" + def create_file(user, path, content, **options) + options[:user] = user + options[:actions] = [{ action: :create, file_path: path, content: content }] - if tree_entry_at(branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end + multi_action(**options) + end - if start_branch_name && - start_project.repository.tree_entry_at(start_branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end - end + def update_file(user, path, content, **options) + previous_path = options.delete(:previous_path) + action = previous_path && previous_path != path ? :move : :update - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :create, - file_path: path, - content: content }]) - end - # rubocop:enable Metrics/ParameterLists + options[:user] = user + options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }] - # rubocop:disable Metrics/ParameterLists - def update_file( - user, path, content, - message:, branch_name:, previous_path:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - action = if previous_path && previous_path != path - :move - else - :update - end - - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: action, - file_path: path, - content: content, - previous_path: previous_path }]) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def remove_file( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :delete, - file_path: path }]) + def delete_file(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :delete, file_path: path }] + + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) + GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| - index = rugged.index - parents = if start_commit - index.read_tree(start_commit.raw_commit.tree) - [start_commit.sha] - else - [] - end + index = Gitlab::Git::Index.new(raw_repository) - actions.each do |act| - git_action(index, act) + if start_commit + index.read_tree(start_commit.raw_commit.tree) + parents = [start_commit.sha] + else + parents = [] + end + + actions.each do |options| + index.public_send(options.delete(:action), options) end options = { - tree: index.write_tree(rugged), + tree: index.write_tree, message: message, parents: parents } @@ -892,7 +822,7 @@ class Repository def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) - author = Gitlab::Git::committer_hash(email: email, name: name) || committer + author = Gitlab::Git.committer_hash(email: email, name: name) || committer { author: author, @@ -941,17 +871,18 @@ class Repository end def revert( - user, commit, branch_name, revert_tree_id = nil, + user, commit, branch_name, start_branch_name: nil, start_project: project) - revert_tree_id ||= check_revert_content(commit, branch_name) - - return false unless revert_tree_id - GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| + revert_tree_id = check_revert_content(commit, start_commit.sha) + unless revert_tree_id + raise Repository::CreateTreeError.new('Failed to revert commit') + end + committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -964,17 +895,18 @@ class Repository end def cherry_pick( - user, commit, branch_name, cherry_pick_tree_id = nil, + user, commit, branch_name, start_branch_name: nil, start_project: project) - cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) - - return false unless cherry_pick_tree_id - GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| + cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) + unless cherry_pick_tree_id + raise Repository::CreateTreeError.new('Failed to cherry-pick commit') + end + committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -998,9 +930,8 @@ class Repository end end - def check_revert_content(target_commit, branch_name) - source_sha = commit(branch_name).sha - args = [target_commit.sha, source_sha] + def check_revert_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] args << { mainline: 1 } if target_commit.merge_commit? revert_index = rugged.revert_commit(*args) @@ -1012,9 +943,8 @@ class Repository tree_id end - def check_cherry_pick_content(target_commit, branch_name) - source_sha = commit(branch_name).sha - args = [target_commit.sha, source_sha] + def check_cherry_pick_content(target_commit, source_sha) + args = [target_commit.sha, source_sha] args << 1 if target_commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) @@ -1074,6 +1004,8 @@ class Repository end def with_repo_branch_commit(start_repository, start_branch_name) + return yield(nil) if start_repository.empty_repo? + branch_name_or_sha = if start_repository == self start_branch_name @@ -1170,30 +1102,6 @@ class Repository blob_data_at(sha, '.gitlab-ci.yml') end - protected - - def tree_entry_at(branch_name, path) - branch_exists?(branch_name) && - # tree_entry is private - raw_repository.send(:tree_entry, commit(branch_name), path) - end - - def check_tree_entry_for_dir(branch_name, path) - return unless branch_exists?(branch_name) - - entry = tree_entry_at(branch_name, path) - - return unless entry - - if entry[:type] == :blob - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists as a file") - else - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists") - end - end - private def blob_data_at(sha, path) @@ -1204,58 +1112,6 @@ class Repository blob.data end - def git_action(index, action) - path = normalize_path(action[:file_path]) - - if action[:action] == :move - previous_path = normalize_path(action[:previous_path]) - end - - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(path)[:mode] - when :move - index.get(previous_path)[:mode] - end - mode ||= 0o100644 - - index.remove(previous_path) if action[:action] == :move - - content = if action[:encoding] == 'base64' - Base64.decode64(action[:content]) - else - action[:content] - end - - detect = CharlockHolmes::EncodingDetector.new.detect(content) if content - - unless detect && detect[:type] == :binary - # When writing to the repo directly as we are doing here, - # the `core.autocrlf` config isn't taken into account. - content.gsub!("\r\n", "\n") if self.autocrlf - end - - oid = rugged.write(content, :blob) - - index.add(path: path, oid: oid, mode: mode) - when :delete - index.remove(path) - end - end - - def normalize_path(path) - pathname = Gitlab::Git::PathHelper.normalize_path(path) - - if pathname.each_filename.include?('..') - raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') - end - - pathname.to_s - end - def refs_directory_exists? return false unless path_with_namespace diff --git a/app/models/route.rb b/app/models/route.rb index 73574a6206b..41e6eb7cb73 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -21,7 +21,7 @@ class Route < ActiveRecord::Base attributes[:path] = route.path.sub(path_was, path) end - if name_changed? && route.name.present? + if name_changed? && name_was.present? && route.name.present? attributes[:name] = route.name.sub(name_was, name) end diff --git a/app/models/service.rb b/app/models/service.rb index facaaf9b331..e73f7e5d1a3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -210,12 +210,11 @@ class Service < ActiveRecord::Base end def self.available_services_names - %w[ + service_names = %w[ asana assembla bamboo buildkite - builds_email bugzilla campfire custom_issue_tracker @@ -232,12 +231,16 @@ class Service < ActiveRecord::Base mattermost pipelines_email pivotaltracker + prometheus pushover redmine slack_slash_commands slack teamcity ] + service_names << 'mock_ci' if Rails.env.development? + + service_names.sort_by(&:downcase) end def self.build_from_template(project_id, template) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2665a7249a3..dbd564e5e7d 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -120,7 +120,7 @@ class Snippet < ActiveRecord::Base end def visibility_level_field - visibility_level + :visibility_level end def no_highlighting? diff --git a/app/models/todo.rb b/app/models/todo.rb index 3dda7948d0b..da3fa7277c2 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -17,7 +17,7 @@ class Todo < ActiveRecord::Base APPROVAL_REQUIRED => :approval_required, UNMERGEABLE => :unmergeable, DIRECTLY_ADDRESSED => :directly_addressed - } + }.freeze belongs_to :author, class_name: "User" belongs_to :note @@ -48,8 +48,14 @@ class Todo < ActiveRecord::Base after_save :keep_around_commit class << self + # Priority sorting isn't displayed in the dropdown, because we don't show + # milestones, but still show something if the user has a URL with that + # selected. def sort(method) - method == "priority" ? order_by_labels_priority : order_by(method) + case method.to_s + when 'priority', 'label_priority' then order_by_labels_priority + else order_by(method) + end end # Order by priority depending on which issue/merge request the Todo belongs to diff --git a/app/models/upload.rb b/app/models/upload.rb new file mode 100644 index 00000000000..13987931b05 --- /dev/null +++ b/app/models/upload.rb @@ -0,0 +1,63 @@ +class Upload < ActiveRecord::Base + # Upper limit for foreground checksum processing + CHECKSUM_THRESHOLD = 100.megabytes + + belongs_to :model, polymorphic: true + + validates :size, presence: true + validates :path, presence: true + validates :model, presence: true + validates :uploader, presence: true + + before_save :calculate_checksum, if: :foreground_checksum? + after_commit :schedule_checksum, unless: :foreground_checksum? + + def self.remove_path(path) + where(path: path).destroy_all + end + + def self.record(uploader) + remove_path(uploader.relative_path) + + create( + size: uploader.file.size, + path: uploader.relative_path, + model: uploader.model, + uploader: uploader.class.to_s + ) + end + + def absolute_path + return path unless relative_path? + + uploader_class.absolute_path(self) + end + + def calculate_checksum + return unless exist? + + self.checksum = Digest::SHA256.file(absolute_path).hexdigest + end + + def exist? + File.exist?(absolute_path) + end + + private + + def foreground_checksum? + size <= CHECKSUM_THRESHOLD + end + + def schedule_checksum + UploadChecksumWorker.perform_async(id) + end + + def relative_path? + !path.start_with?('/') + end + + def uploader_class + Object.const_get(uploader) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index f614eb66e1f..8c7ad5d5174 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,6 +21,7 @@ class User < ActiveRecord::Base default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false + default_value_for :project_view, :files attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -81,7 +82,6 @@ class User < ActiveRecord::Base has_many :authorized_projects, through: :project_authorizations, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id - has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id @@ -95,16 +95,22 @@ class User < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy has_many :award_emoji, dependent: :destroy + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" + # Issues that a user owns are expected to be moved to the "ghost" user before + # the user is destroyed. If the user owns any issues during deletion, this + # should be treated as an exceptional condition. + has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id + # # Validations # # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true - validates_confirmation_of :email + validates :email, confirmation: true validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true @@ -184,6 +190,7 @@ class User < ActiveRecord::Base end mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy # Scopes scope :admins, -> { where(admin: true) } @@ -317,8 +324,7 @@ class User < ActiveRecord::Base end def find_by_personal_access_token(token_string) - personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string - personal_access_token&.user + PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user end # Returns a user for the given SSH key. @@ -334,9 +340,34 @@ class User < ActiveRecord::Base def reference_pattern %r{ #{Regexp.escape(reference_prefix)} - (?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) + (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}) }x end + + # Return (create if necessary) the ghost user. The ghost user + # owns records previously belonging to deleted users. + def ghost + unique_internal(where(ghost: true), 'ghost', 'ghost%s@example.com') do |u| + u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.' + u.name = 'Ghost User' + end + end + end + + def self.internal_attributes + [:ghost] + end + + def internal? + self.class.internal_attributes.any? { |a| self[a] } + end + + def self.internal + where(Hash[internal_attributes.zip([true] * internal_attributes.size)]) + end + + def self.non_internal + where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)]) end # @@ -457,7 +488,7 @@ class User < ActiveRecord::Base Group.member_descendants(id) end - def nested_projects + def nested_groups_projects Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). member_descendants(id) end @@ -540,14 +571,14 @@ class User < ActiveRecord::Base end def can_create_group? - can?(:create_group, nil) + can?(:create_group) end def can_select_namespace? several_namespaces? || admin end - def can?(action, subject) + def can?(action, subject = :global) Ability.allowed?(self, action, subject) end @@ -580,8 +611,8 @@ class User < ActiveRecord::Base if project.repository.branch_exists?(event.branch_name) merge_requests = MergeRequest.where("created_at >= ?", event.created_at). - where(source_project_id: project.id, - source_branch: event.branch_name) + where(source_project_id: project.id, + source_branch: event.branch_name) merge_requests.empty? end end @@ -846,7 +877,7 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin runner_ids = Ci::RunnerProject. - where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})"). + where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})"). select(:runner_id) Ci::Runner.specific.where(id: runner_ids) end @@ -932,6 +963,14 @@ class User < ActiveRecord::Base self.admin = (new_level == 'admin') end + protected + + # override, from Devise::Validatable + def password_required? + return false if internal? + super + end + private def ci_projects_union @@ -999,4 +1038,43 @@ class User < ActiveRecord::Base super end end + + def self.unique_internal(scope, username, email_pattern, &b) + scope.first || create_unique_internal(scope, username, email_pattern, &b) + end + + def self.create_unique_internal(scope, username, email_pattern, &creation_block) + # Since we only want a single one of these in an instance, we use an + # exclusive lease to ensure than this block is never run concurrently. + lease_key = "user:unique_internal:#{username}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. To prevent hammering Redis too + # much we'll wait for a bit between retries. + sleep(1) + end + + # Recheck if the user is already present. One might have been + # added between the time we last checked (first line of this method) + # and the time we acquired the lock. + existing_user = uncached { scope.first } + return existing_user if existing_user.present? + + uniquify = Uniquify.new + + username = uniquify.string(username) { |s| User.find_by_username(s) } + + email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s| + User.find_by_email(s) + end + + scope.create( + username: username, + email: email, + &creation_block + ) + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 2caebb496db..c771c22f46a 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -149,7 +149,13 @@ class WikiPage end # Returns boolean True or False if this instance - # has been fully saved to disk or not. + # is the latest commit version of the page. + def latest? + !historical? + end + + # Returns boolean True or False if this instance + # has been fully created on disk or not. def persisted? @persisted == true end @@ -220,6 +226,8 @@ class WikiPage end def save(method, *args) + saved = false + project_wiki = wiki if valid? && project_wiki.send(method, *args) @@ -237,10 +245,10 @@ class WikiPage set_attributes @persisted = true + saved = true else errors.add(:base, project_wiki.error_message) if project_wiki.error_message - @persisted = false end - @persisted + saved end end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index b9f1c29c32e..8890409d056 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -6,14 +6,16 @@ class BasePolicy @cannot_set = cannot_set end - def size - to_set.size - end + delegate :size, to: :to_set def self.empty new(Set.new, Set.new) end + def self.none + empty.freeze + end + def can?(ability) @can_set.include?(ability) && !@cannot_set.include?(ability) end @@ -51,7 +53,8 @@ class BasePolicy end def self.class_for(subject) - return GlobalPolicy if subject.nil? + return GlobalPolicy if subject == :global + raise ArgumentError, 'no policy for nil' if subject.nil? if subject.class.try(:presenter?) subject = subject.subject @@ -81,7 +84,7 @@ class BasePolicy end def abilities - return RuleSet.empty if @user && @user.blocked? + return RuleSet.none if @user && @user.blocked? return anonymous_abilities if @user.nil? collect_rules { rules } end diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb new file mode 100644 index 00000000000..c90c9ac0583 --- /dev/null +++ b/app/policies/ci/trigger_policy.rb @@ -0,0 +1,13 @@ +module Ci + class TriggerPolicy < BasePolicy + def rules + delegate! @subject.project + + if can?(:admin_build) + can! :admin_trigger if @subject.owner.blank? || + @subject.owner == @user + can! :manage_trigger + end + end + end +end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 3c2fbe6b56b..cb72c2b4590 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -4,5 +4,12 @@ class GlobalPolicy < BasePolicy can! :create_group if @user.can_create_group can! :read_users_list + + unless @user.blocked? || @user.internal? + can! :log_in unless @user.access_locked? + can! :access_api + can! :access_git + can! :receive_notifications + end end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 0be6e113655..4cc21696eb6 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -33,8 +33,6 @@ class GroupPolicy < BasePolicy if globally_viewable && @subject.request_access_enabled && !member can! :request_access end - - additional_rules!(master) end def can_read_group? @@ -45,8 +43,4 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(@subject).execute(@user).any? end - - def additional_rules!(master) - # This is meant to be overriden in EE - end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 03a2499e263..229846e368c 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -3,6 +3,14 @@ class UserPolicy < BasePolicy def rules can! :read_user if @user || !restricted_public_level? + + if @user + if @user.admin? || @subject == @user + can! :destroy_user + end + + cannot! :destroy_user if @subject.ghost? + end end def restricted_public_level? diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb new file mode 100644 index 00000000000..86ac513b3c0 --- /dev/null +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -0,0 +1,60 @@ +module Projects + module Settings + class DeployKeysPresenter < Gitlab::View::Presenter::Simple + presents :project + delegate :size, to: :enabled_keys, prefix: true + delegate :size, to: :available_project_keys, prefix: true + delegate :size, to: :available_public_keys, prefix: true + + def new_key + @key ||= DeployKey.new + end + + def enabled_keys + @enabled_keys ||= project.deploy_keys + end + + def any_keys_enabled? + enabled_keys.any? + end + + def available_keys + @available_keys ||= current_user.accessible_deploy_keys - enabled_keys + end + + def available_project_keys + @available_project_keys ||= current_user.project_deploy_keys - enabled_keys + end + + def any_available_project_keys_enabled? + available_project_keys.any? + end + + def key_available?(deploy_key) + available_keys.include?(deploy_key) + end + + def available_public_keys + return @available_public_keys if defined?(@available_public_keys) + + @available_public_keys ||= DeployKey.are_public - enabled_keys + + # Public keys that are already used by another accessible project are already + # in @available_project_keys. + @available_public_keys -= available_project_keys + end + + def any_available_public_keys_enabled? + available_public_keys.any? + end + + def to_partial_path + 'projects/deploy_keys/index' + end + + def form_partial_path + 'projects/deploy_keys/form' + end + end + end +end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index b5384e6462b..5bcbe285052 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity path_to(:retry_namespace_project_build, build) end - expose :play_path, if: ->(build, _) { build.manual? } do |build| + expose :play_path, if: ->(build, _) { build.playable? } do |build| path_to(:play_namespace_project_build, build) end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 7445298c714..5f80ab397a9 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -6,7 +6,7 @@ class MergeRequestEntity < IssuableEntity expose :merge_params expose :merge_status expose :merge_user_id - expose :merge_when_build_succeeds + expose :merge_when_pipeline_succeeds expose :source_branch expose :source_project_id expose :target_branch diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 2bc6cf3266e..ab2d3d5a3ec 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,5 +1,5 @@ class PipelineSerializer < BaseSerializer - class InvalidResourceError < StandardError; end + InvalidResourceError = Class.new(StandardError) entity PipelineEntity diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index ddaaed90e5b..b2a543daa00 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -1,10 +1,16 @@ -AccessTokenValidationService = Struct.new(:token) do +class AccessTokenValidationService # Results: VALID = :valid EXPIRED = :expired REVOKED = :revoked INSUFFICIENT_SCOPE = :insufficient_scope + attr_reader :token + + def initialize(token) + @token = token + end + def validate(scopes: []) if token.expired? return EXPIRED diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 5b2fcdf3b16..08fe6e3293e 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -2,7 +2,7 @@ module Auth class ContainerRegistryAuthenticationService < BaseService include Gitlab::CurrentSettings - AUDIENCE = 'container_registry' + AUDIENCE = 'container_registry'.freeze def execute(authentication_abilities:) @authentication_abilities = authentication_abilities diff --git a/app/services/base_service.rb b/app/services/base_service.rb index fa45506317e..745c2c4b681 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -28,9 +28,7 @@ class BaseService SystemHooksService.new end - def repository - project.repository - end + delegate :repository, to: :project # Add an error to the specified model for restricted visibility levels def deny_visibility_level(model, denied_visibility_level = nil) diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 8a94c54b6ab..83f51947bd4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -5,7 +5,7 @@ module Boards issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? - issues + issues.order_by_position_and_priority end private @@ -26,7 +26,6 @@ module Boards def filter_params set_default_scope - set_default_sort set_project set_state @@ -37,10 +36,6 @@ module Boards params[:scope] = 'all' end - def set_default_sort - params[:sort] = 'priority' - end - def set_project params[:project_id] = project.id end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 96554a92a02..2a9981ab884 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -3,7 +3,7 @@ module Boards class MoveService < BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false unless valid_move? + return false if issue_params.empty? update_service.execute(issue) end @@ -14,7 +14,7 @@ module Boards @board ||= project.boards.find(params[:board_id]) end - def valid_move? + def move_between_lists? moving_from_list.present? && moving_to_list.present? && moving_from_list != moving_to_list end @@ -32,11 +32,19 @@ module Boards end def issue_params - { - add_label_ids: add_label_ids, - remove_label_ids: remove_label_ids, - state_event: issue_state - } + attrs = {} + + if move_between_lists? + attrs.merge!( + add_label_ids: add_label_ids, + remove_label_ids: remove_label_ids, + state_event: issue_state, + ) + end + + attrs[:move_between_iids] = move_between_iids if move_between_iids + + attrs end def issue_state @@ -58,6 +66,12 @@ module Boards Array(label_ids).compact end + + def move_between_iids + return unless params[:move_after_iid] || params[:move_before_iid] + + [params[:move_after_iid], params[:move_before_iid]] + end end end end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb index b7da3f8e7eb..70fb2c5e38f 100644 --- a/app/services/ci/create_pipeline_builds_service.rb +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -10,9 +10,7 @@ module Ci end end - def project - pipeline.project - end + delegate :project, to: :pipeline private diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index e3bc9847200..38a85e9fc42 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -59,7 +59,8 @@ module Ci private def skip_ci? - pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message + return false unless pipeline.git_commit_message + pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i end def commit diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 6af3c1ca5b1..dca5aa9f5d7 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -3,7 +3,7 @@ module Ci def execute(project, trigger, ref, variables = nil) trigger_request = trigger.trigger_requests.create(variables: variables) - pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref). + pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref). execute(ignore_skip_ci: true, trigger_request: trigger_request) if pipeline.persisted? trigger_request diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb deleted file mode 100644 index 240ddabec36..00000000000 --- a/app/services/ci/image_for_build_service.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Ci - class ImageForBuildService - def execute(project, opts) - ref = opts[:ref] - sha = opts[:sha] || ref_sha(project, ref) - pipelines = project.pipelines.where(sha: sha) - - image_name = image_for_status(pipelines.latest_status(ref)) - image_path = Rails.root.join('public/ci', image_name) - - OpenStruct.new(path: image_path, name: image_name) - end - - private - - def ref_sha(project, ref) - project.commit(ref).try(:sha) if ref - end - - def image_for_status(status) - status ||= 'unknown' - 'build-' + status + ".svg" - end - end -end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 79eb97b7b55..2935d00c075 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -22,6 +22,8 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) + return if HasStatus::BLOCKED_STATUS == current_status + if HasStatus::COMPLETED_STATUSES.include?(current_status) created_builds_in_stage(index).select do |build| Gitlab::OptimisticLocking.retry_lock(build) do |subject| @@ -33,7 +35,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.enqueue + build.action? ? build.actionize : build.enqueue true else build.skip @@ -49,6 +51,8 @@ module Ci %w[failed] when 'always' %w[success failed skipped] + when 'manual' + %w[success] else [] end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb deleted file mode 100644 index 6f03bf2be13..00000000000 --- a/app/services/ci/register_build_service.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Ci - # This class responsible for assigning - # proper pending build to runner on runner API request - class RegisterBuildService - include Gitlab::CurrentSettings - - attr_reader :runner - - Result = Struct.new(:build, :valid?) - - def initialize(runner) - @runner = runner - end - - def execute - builds = - if runner.shared? - builds_for_shared_runner - else - builds_for_specific_runner - end - - build = builds.find do |build| - runner.can_pick?(build) - end - - if build - # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. - build.runner_id = runner.id - build.run! - end - - Result.new(build, true) - - rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError - Result.new(build, false) - end - - private - - def builds_for_shared_runner - new_builds. - # don't run projects which have not enabled shared runners and builds - joins(:project).where(projects: { shared_runners_enabled: true }). - joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). - where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). - - # Implement fair scheduling - # this returns builds that are ordered by number of running builds - # we prefer projects that don't use shared runners at all - joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). - order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') - end - - def builds_for_specific_runner - new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') - end - - def running_builds_for_shared_runners - Ci::Build.running.where(runner: Ci::Runner.shared). - group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds') - end - - def new_builds - Ci::Build.pending.unstarted - end - - def shared_runner_build_limits_feature_enabled? - ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true' - end - end -end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb new file mode 100644 index 00000000000..d6a4280ce4c --- /dev/null +++ b/app/services/ci/register_job_service.rb @@ -0,0 +1,85 @@ +module Ci + # This class responsible for assigning + # proper pending build to runner on runner API request + class RegisterJobService + include Gitlab::CurrentSettings + + attr_reader :runner + + Result = Struct.new(:build, :valid?) + + def initialize(runner) + @runner = runner + end + + def execute + builds = + if runner.shared? + builds_for_shared_runner + else + builds_for_specific_runner + end + + valid = true + + builds.find do |build| + next unless runner.can_pick?(build) + + begin + # In case when 2 runners try to assign the same build, second runner will be declined + # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. + build.runner_id = runner.id + build.run! + + return Result.new(build, true) + rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError + # We are looping to find another build that is not conflicting + # It also indicates that this build can be picked and passed to runner. + # If we don't do it, basically a bunch of runners would be competing for a build + # and thus we will generate a lot of 409. This will increase + # the number of generated requests, also will reduce significantly + # how many builds can be picked by runner in a unit of time. + # In case we hit the concurrency-access lock, + # we still have to return 409 in the end, + # to make sure that this is properly handled by runner. + valid = false + end + end + + Result.new(nil, valid) + end + + private + + def builds_for_shared_runner + new_builds. + # don't run projects which have not enabled shared runners and builds + joins(:project).where(projects: { shared_runners_enabled: true }). + joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id'). + where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). + + # Implement fair scheduling + # this returns builds that are ordered by number of running builds + # we prefer projects that don't use shared runners at all + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id"). + order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') + end + + def builds_for_specific_runner + new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') + end + + def running_builds_for_shared_runners + Ci::Build.running.where(runner: Ci::Runner.shared). + group(:project_id).select(:project_id, 'count(*) AS running_builds') + end + + def new_builds + Ci::Build.pending.unstarted + end + + def shared_runner_build_limits_feature_enabled? + ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true' + end + end +end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 4b47ee489cf..89da05b72bb 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -1,17 +1,9 @@ module Ci class RetryBuildService < ::BaseService - CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name - allow_failure stage stage_idx trigger_request - yaml_variables when environment coverage_regex] - .freeze - - REJECT_ATTRIBUTES = %i[id status user token coverage trace runner - artifacts_file artifacts_metadata artifacts_size - created_at updated_at started_at finished_at - queued_at erased_by erased_at].freeze - - IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url - deploy job_id description].freeze + CLONE_ACCESSORS = %i[pipeline project ref tag options commands name + allow_failure stage stage_idx trigger_request + yaml_variables when environment coverage_regex + description tag_list].freeze def execute(build) reprocess(build).tap do |new_build| @@ -30,7 +22,7 @@ module Ci raise Gitlab::Access::AccessDeniedError end - attributes = CLONE_ATTRIBUTES.map do |attribute| + attributes = CLONE_ACCESSORS.map do |attribute| [attribute, build.send(attribute)] end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 2c5e130e5aa..574561adc4c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -1,5 +1,7 @@ module Ci class RetryPipelineService < ::BaseService + include Gitlab::OptimisticLocking + def execute(pipeline) unless can?(current_user, :update_pipeline, pipeline) raise Gitlab::Access::AccessDeniedError @@ -12,6 +14,10 @@ module Ci .reprocess(build) end + pipeline.builds.skipped.find_each do |skipped| + retry_optimistic_lock(skipped) { |build| build.process } + end + MergeRequests::AddTodoWhenBuildFailsService .new(project, current_user) .close_all(pipeline) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 25e22f14e60..1297a792259 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -1,16 +1,16 @@ module Commits class ChangeService < ::BaseService - class ValidationError < StandardError; end - class ChangeError < StandardError; end + ValidationError = Class.new(StandardError) + ChangeError = Class.new(StandardError) def execute @start_project = params[:start_project] || @project @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit = params[:commit] - @create_merge_request = params[:create_merge_request].present? - check_push_permissions unless @create_merge_request + check_push_permissions + commit rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError, ChangeError => ex @@ -26,34 +26,21 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - if @create_merge_request - into = @commit.public_send("#{action}_branch_name") - tree_branch = @start_branch - else - into = tree_branch = @target_branch - end - - tree_id = repository.public_send( - "check_#{action}_content", @commit, tree_branch) - - if tree_id - validate_target_branch(into) if @create_merge_request + validate_target_branch if different_branch? - repository.public_send( - action, - current_user, - @commit, - into, - tree_id, - start_project: @start_project, - start_branch_name: @start_branch) + repository.public_send( + action, + current_user, + @commit, + @target_branch, + start_project: @start_project, + start_branch_name: @start_branch) - success - else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. + success + rescue Repository::CreateTreeError + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." - raise ChangeError, error_msg - end + raise ChangeError, error_msg end def check_push_permissions @@ -66,16 +53,17 @@ module Commits true end - def validate_target_branch(new_branch) - # Temporary branch exists and contains the change commit - return if repository.find_branch(new_branch) - + def validate_target_branch result = ValidateNewBranchService.new(@project, current_user) - .execute(new_branch) + .execute(@target_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" end end + + def different_branch? + @start_branch != @target_branch || @start_project != @project + end end end diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb new file mode 100644 index 00000000000..297c7d696c3 --- /dev/null +++ b/app/services/concerns/issues/resolve_discussions.rb @@ -0,0 +1,32 @@ +module Issues + module ResolveDiscussions + attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id + + def filter_resolve_discussion_params + @merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of) + @discussion_to_resolve_id ||= params.delete(:discussion_to_resolve) + end + + def merge_request_to_resolve_discussions_of + return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of) + + @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id). + execute. + find_by(iid: merge_request_to_resolve_discussions_of_iid) + end + + def discussions_to_resolve + return [] unless merge_request_to_resolve_discussions_of + + @discussions_to_resolve ||= + if discussion_to_resolve_id + discussion_or_nil = merge_request_to_resolve_discussions_of + .find_diff_discussion(discussion_to_resolve_id) + Array(discussion_or_nil) + else + merge_request_to_resolve_discussions_of + .resolvable_discussions + end + end + end +end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 77459d8779d..b07338d500a 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,5 +1,7 @@ class CreateBranchService < BaseService def execute(branch_name, ref) + create_master_branch if project.empty_repo? + result = ValidateNewBranchService.new(project, current_user) .execute(branch_name) @@ -19,4 +21,16 @@ class CreateBranchService < BaseService def success(branch) super().merge(branch: branch) end + + private + + def create_master_branch + project.repository.commit_file( + current_user, + '/README.md', + '', + message: 'Add README.md', + branch_name: 'master', + update: false) + end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 0a25f56d24c..c8a60422bf4 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,6 +1,6 @@ module Files class BaseService < ::BaseService - class ValidationError < StandardError; end + ValidationError = Class.new(StandardError) def execute @start_project = params[:start_project] || @project @@ -58,16 +58,12 @@ module Files raise_error("You are not allowed to push into this branch") end - unless project.empty_repo? - unless @start_project.repository.branch_exists?(@start_branch) - raise_error('You can only create or edit files when you are on a branch') - end + if !@start_project.empty_repo? && !@start_project.repository.branch_exists?(@start_branch) + raise ValidationError, 'You can only create or edit files when you are on a branch' + end - if different_branch? - if repository.branch_exists?(@target_branch) - raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') - end - end + if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) + raise ValidationError, "A branch called #{@branch_name} already exists. Switch to that branch in order to make changes" end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 858de5f0538..083ffdc634c 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,7 +1,7 @@ module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir( + repository.create_dir( current_user, @file_path, message: @commit_message, diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 88dd7bbaedb..65b5537fb68 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,13 +1,12 @@ module Files class CreateService < Files::BaseService def commit - repository.commit_file( + repository.create_file( current_user, @file_path, @file_content, message: @commit_message, branch_name: @target_branch, - update: false, author_email: @author_email, author_name: @author_name, start_project: @start_project, @@ -17,6 +16,10 @@ module Files def validate super + if @file_content.nil? + raise_error("You must provide content.") + end + if @file_path =~ Gitlab::Regex.directory_traversal_regex raise_error( 'Your changes could not be committed, because the file name ' + diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb index c3be806a42d..e294659bc98 100644 --- a/app/services/files/destroy_service.rb +++ b/app/services/files/destroy_service.rb @@ -1,7 +1,7 @@ module Files class DestroyService < Files::BaseService def commit - repository.remove_file( + repository.delete_file( current_user, @file_path, message: @commit_message, diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index af6da5b9d56..700f9f4f6f0 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -1,6 +1,8 @@ module Files class MultiService < Files::BaseService - class FileChangedError < StandardError; end + FileChangedError = Class.new(StandardError) + + ACTIONS = %w[create update delete move].freeze def commit repository.multi_action( @@ -21,10 +23,19 @@ module Files super params[:actions].each_with_index do |action, index| + if ACTIONS.include?(action[:action].to_s) + action[:action] = action[:action].to_sym + else + raise_error("Unknown action type `#{action[:action]}`.") + end + unless action[:file_path].present? raise_error("You must specify a file_path.") end + action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') + action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') + regex_check(action[:file_path]) regex_check(action[:previous_path]) if action[:previous_path] @@ -43,8 +54,6 @@ module Files validate_delete(action) when :move validate_move(action, index) - else - raise_error("Unknown action type `#{action[:action]}`.") end end end @@ -92,6 +101,20 @@ module Files if repository.blob_at_branch(params[:branch], action[:file_path]) raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") end + + if action[:content].nil? + raise_error("You must provide content.") + end + end + + def validate_update(action) + if action[:content].nil? + raise_error("You must provide content.") + end + + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") + end end def validate_delete(action) @@ -114,11 +137,5 @@ module Files params[:actions][index][:content] = blob.data end end - - def validate_update(action) - if file_has_changed? - raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") - end - end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a71fe61a4b6..fbbab97632e 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,6 +1,6 @@ module Files class UpdateService < Files::BaseService - class FileChangedError < StandardError; end + FileChangedError = Class.new(StandardError) def commit repository.update_file(current_user, @file_path, @file_content, @@ -18,6 +18,10 @@ module Files def validate super + if @file_content.nil? + raise_error("You must provide content.") + end + if file_has_changed? raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 27bcc047601..ed6ea638235 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -56,12 +56,16 @@ class GitOperationService start_project: repository.project, &block) - check_with_branch_arguments!( - branch_name, start_branch_name, start_project) + start_repository = start_project.repository + start_branch_name = nil if start_repository.empty_repo? + + if start_branch_name && !start_repository.branch_exists?(start_branch_name) + raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.path_with_namespace}" + end update_branch_with_hooks(branch_name) do repository.with_repo_branch_commit( - start_project.repository, + start_repository, start_branch_name || branch_name, &block) end @@ -149,31 +153,4 @@ class GitOperationService repository.raw_repository.autocrlf = :input end end - - def check_with_branch_arguments!( - branch_name, start_branch_name, start_project) - return if repository.branch_exists?(branch_name) - - if repository.project != start_project - unless start_branch_name - raise ArgumentError, - 'Should also pass :start_branch_name if' + - ' :start_project is different from current project' - end - - unless start_project.repository.branch_exists?(start_branch_name) - raise ArgumentError, - "Cannot find branch #{branch_name} nor" \ - " #{start_branch_name} from" \ - " #{start_project.path_with_namespace}" - end - elsif start_branch_name - unless repository.branch_exists?(start_branch_name) - raise ArgumentError, - "Cannot find branch #{branch_name} nor" \ - " #{start_branch_name} from" \ - " #{repository.project.path_with_namespace}" - end - end - end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index dbe2fda27b5..bc7431c89a8 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -99,6 +99,8 @@ class GitPushService < BaseService UpdateMergeRequestsWorker .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) + SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) + EventCreateService.new.push(@project, current_user, build_push_data) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index febeb661fb5..c4e9b8fd8e0 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -2,6 +2,7 @@ module Groups class CreateService < Groups::BaseService def initialize(user, params = {}) @current_user, @params = user, params.dup + @chat_team = @params.delete(:create_chat_team) end def execute @@ -20,9 +21,23 @@ module Groups end @group.name ||= @group.path.dup + + if create_chat_team? + response = Mattermost::CreateTeamService.new(@group, current_user).execute + return @group if @group.errors.any? + + @group.build_chat_team(name: response['name'], team_id: response['id']) + end + @group.save @group.add_owner(current_user) @group end + + private + + def create_chat_team? + Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil? + end end end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 2e2d7f884ac..497fdb09cdc 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -18,7 +18,8 @@ module Groups end group.children.each do |group| - DestroyService.new(group, current_user).async_execute + # This needs to be synchronous since the namespace gets destroyed below + DestroyService.new(group, current_user).execute end group.really_destroy! diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 9500faf2862..b071a398481 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -203,6 +203,7 @@ class IssuableBaseService < BaseService change_state(issuable) change_subscription(issuable) change_todo(issuable) + toggle_award(issuable) filter_params(issuable) old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a @@ -210,7 +211,7 @@ class IssuableBaseService < BaseService label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) - if params.present? + if issuable.changed? || params.present? issuable.assign_attributes(params.merge(updated_by: current_user)) before_update(issuable) @@ -263,6 +264,14 @@ class IssuableBaseService < BaseService end end + def toggle_award(issuable) + award = params.delete(:emoji_award) + if award + todo_service.new_award_emoji(issuable, current_user) + issuable.toggle_award_emoji(award, current_user) + end + end + def has_changes?(issuable, old_labels: []) valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 35af867a098..ee1b40db718 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,13 +1,5 @@ module Issues class BaseService < ::IssuableBaseService - attr_reader :merge_request_for_resolving_discussions - - def initialize(*args) - super - - @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions) - end - def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.build(issue) diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 7cd927d8005..77bced4bd5c 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -1,50 +1,56 @@ module Issues class BuildService < Issues::BaseService + include ResolveDiscussions + def execute + filter_resolve_discussion_params @issue = project.issues.new(issue_params) end - def issue_params_with_info_from_merge_request - return {} unless merge_request_for_resolving_discussions + def issue_params_with_info_from_discussions + return {} unless merge_request_to_resolve_discussions_of - { title: title_from_merge_request, description: description_from_merge_request } + { title: title_from_merge_request, description: description_for_discussions } end def title_from_merge_request - "Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" + "Follow-up from \"#{merge_request_to_resolve_discussions_of.title}\"" end - def description_from_merge_request - if merge_request_for_resolving_discussions.resolvable_discussions.empty? + def description_for_discussions + if discussions_to_resolve.empty? return "There are no unresolved discussions. "\ - "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" + "Review the conversation in #{merge_request_to_resolve_discussions_of.to_reference}" end - description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" + description = "The following #{'discussion'.pluralize(discussions_to_resolve.size)} "\ + "from #{merge_request_to_resolve_discussions_of.to_reference} "\ + "should be addressed:" + [description, *items_for_discussions].join("\n\n") end def items_for_discussions - merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } + discussions_to_resolve.map { |discussion| item_for_discussion(discussion) } end def item_for_discussion(discussion) - first_note = discussion.first_note_to_resolve + first_note = discussion.first_note_to_resolve || discussion.first_note other_note_count = discussion.notes.size - 1 - creation_time = first_note.created_at.to_s(:medium) note_url = Gitlab::UrlBuilder.build(first_note) - discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " + discussion_info = "- [ ] #{first_note.author.to_reference} commented on a [discussion](#{note_url}): " discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call - quote = ">>>\n#{note_without_block_quotes}\n>>>" + spaces = ' ' * 4 + quote = note_without_block_quotes.lines.map { |line| "#{spaces}> #{line}" }.join [discussion_info, quote].join("\n\n") end def issue_params - @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params) + @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params) end def whitelisted_issue_params diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 366b3572738..3cf4b82b9f2 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -1,18 +1,20 @@ module Issues class CreateService < Issues::BaseService include SpamCheckService + include ResolveDiscussions def execute - filter_spam_check_params + @issue = BuildService.new(project, current_user, params).execute - issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) - @issue = BuildService.new(project, current_user, issue_attributes).execute + filter_spam_check_params + filter_resolve_discussion_params create(@issue) end def before_create(issue) spam_check(issue, current_user) + issue.move_to_end end def after_create(issuable) @@ -20,17 +22,16 @@ module Issues notification_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user) user_agent_detail_service.create - - if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user) - resolve_discussions_in_merge_request(issuable) - end + resolve_discussions_with_issue(issuable) end - def resolve_discussions_in_merge_request(issue) + def resolve_discussions_with_issue(issue) + return if discussions_to_resolve.empty? + Discussions::ResolveService.new(project, current_user, - merge_request: merge_request_for_resolving_discussions, + merge_request: merge_request_to_resolve_discussions_of, follow_up_issue: issue). - execute(merge_request_for_resolving_discussions.resolvable_discussions) + execute(discussions_to_resolve) end private diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a2a5f57d069..711f4035c55 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -1,6 +1,6 @@ module Issues class MoveService < Issues::BaseService - class MoveError < StandardError; end + MoveError = Class.new(StandardError) def execute(issue, new_project) @old_issue = issue diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 22e32b13259..a444c78b609 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -3,8 +3,8 @@ module Issues include SpamCheckService def execute(issue) + handle_move_between_iids(issue) filter_spam_check_params - update(issue) end @@ -37,11 +37,13 @@ module Issues end added_labels = issue.labels - old_labels + if added_labels.present? notification_service.relabeled_issue(issue, added_labels, current_user) end added_mentions = issue.mentioned_users - old_mentioned_users + if added_mentions.present? notification_service.new_mentions_in_issue(issue, added_mentions, current_user) end @@ -55,8 +57,24 @@ module Issues Issues::CloseService end + def handle_move_between_iids(issue) + return unless params[:move_between_iids] + + after_iid, before_iid = params.delete(:move_between_iids) + + issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid + issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid + + issue.move_between(issue_before, issue_after) + end + private + def get_issue_if_allowed(project, iid) + issue = project.issues.find_by(iid: iid) + issue if can?(current_user, :update_issue, issue) + end + def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) end diff --git a/app/services/mattermost/create_team_service.rb b/app/services/mattermost/create_team_service.rb new file mode 100644 index 00000000000..e3206810f3a --- /dev/null +++ b/app/services/mattermost/create_team_service.rb @@ -0,0 +1,14 @@ +module Mattermost + class CreateTeamService < ::BaseService + def initialize(group, current_user) + @group, @current_user = group, current_user + end + + def execute + # The user that creates the team will be Team Admin + Mattermost::Team.new(current_user).create(@group.mattermost_team_params) + rescue Mattermost::ClientError => e + @group.errors.add(:mattermost_team, e.message) + end + end +end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 431da8372c9..2e089149ca8 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -4,7 +4,7 @@ module Members attr_accessor :source - ALLOWED_SCOPES = %i[members requesters all] + ALLOWED_SCOPES = %i[members requesters all].freeze def initialize(source, current_user, params = {}) @source = source diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 9d4739e37bb..fdce542bd9e 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -6,7 +6,7 @@ module MergeRequests merge_request.source_project = find_source_project merge_request.target_project = find_target_project merge_request.target_branch = find_target_branch - merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified? + merge_request.can_be_created = branches_valid? compare_branches if branches_present? assign_title_and_description if merge_request.can_be_created diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 1262ecbc29a..f00a33969a8 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -7,6 +7,8 @@ module MergeRequests end def execute(changes) + return [] unless project.printing_merge_request_link_enabled + branches = get_branches(changes) merge_requests_map = opened_merge_requests_from_source_branches(branches) branches.map do |branch| @@ -23,10 +25,7 @@ module MergeRequests def opened_merge_requests_from_source_branches(branches) merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches) - merge_requests.inject({}) do |hash, mr| - hash[mr.source_branch] = mr - hash - end + merge_requests.index_by(&:source_branch) end def get_branches(changes) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 3da1b657a41..fac3ac7a4c7 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,6 +6,8 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService + MergeError = Class.new(StandardError) + attr_reader :merge_request, :source def execute(merge_request) @@ -27,6 +29,8 @@ module MergeRequests success end end + rescue MergeError => e + log_merge_error(e.message, save_message_on_model: true) end private @@ -42,19 +46,13 @@ module MergeRequests commit_id = repository.merge(current_user, source, merge_request, options) - if commit_id - merge_request.update(merge_commit_sha: commit_id) - else - log_merge_error('Conflicts detected during merge', save_message_on_model: true) - false - end + raise MergeError, 'Conflicts detected during merge' unless commit_id + + merge_request.update(merge_commit_sha: commit_id) rescue GitHooksService::PreReceiveError => e - log_merge_error(e.message, save_message_on_model: true) - false + raise MergeError, e.message rescue StandardError => e - merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") - log_merge_error(e.message) - false + raise MergeError, "Something went wrong during merge: #{e.message}" ensure merge_request.update(in_progress_merge_commit_sha: nil) end diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index 5616edf8b4a..aed5287940e 100644 --- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -1,18 +1,18 @@ module MergeRequests class MergeWhenPipelineSucceedsService < MergeRequests::BaseService - # Marks the passed `merge_request` to be merged when the build succeeds or + # Marks the passed `merge_request` to be merged when the pipeline succeeds or # updates the params for the automatic merge def execute(merge_request) merge_request.merge_params.merge!(params) # The service is also called when the merge params are updated. - already_approved = merge_request.merge_when_build_succeeds? + already_approved = merge_request.merge_when_pipeline_succeeds? unless already_approved - merge_request.merge_when_build_succeeds = true - merge_request.merge_user = @current_user + merge_request.merge_when_pipeline_succeeds = true + merge_request.merge_user = @current_user - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) + SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) end merge_request.save @@ -23,8 +23,12 @@ module MergeRequests return unless pipeline.success? pipeline_merge_requests(pipeline) do |merge_request| - next unless merge_request.merge_when_build_succeeds? - next unless merge_request.mergeable? + next unless merge_request.merge_when_pipeline_succeeds? + + unless merge_request.mergeable? + todo_service.merge_request_became_unmergeable(merge_request) + next + end MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end @@ -32,9 +36,9 @@ module MergeRequests # Cancels the automatic merge def cancel(merge_request) - if merge_request.merge_when_build_succeeds? && merge_request.open? - merge_request.reset_merge_when_build_succeeds - SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + if merge_request.merge_when_pipeline_succeeds? && merge_request.open? + merge_request.reset_merge_when_pipeline_succeeds + SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) success else diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 581d18032e6..1131d6f4913 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -11,7 +11,7 @@ module MergeRequests # empty diff during a manual merge close_merge_requests reload_merge_requests - reset_merge_when_build_succeeds + reset_merge_when_pipeline_succeeds mark_pending_todos_done cache_merge_requests_closing_issues @@ -78,8 +78,8 @@ module MergeRequests end end - def reset_merge_when_build_succeeds - merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) + def reset_merge_when_pipeline_succeeds + merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end def mark_pending_todos_done diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index d22a1d3e0ad..82cd89d9a0b 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,7 +1,6 @@ module MergeRequests class ResolveService < MergeRequests::BaseService - class MissingFiles < Gitlab::Conflict::ResolutionError - end + MissingFiles = Class.new(Gitlab::Conflict::ResolutionError) attr_accessor :conflicts, :rugged, :merge_index, :merge_request diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index b4f8b33d564..61d66a26932 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -8,14 +8,6 @@ module Notes note.author = current_user note.system = false - if note.award_emoji? - noteable = note.noteable - if noteable.user_can_award?(current_user, note.award_emoji_name) - todo_service.new_award_emoji(noteable, current_user) - return noteable.create_award_emoji(note.award_emoji_name, current_user) - end - end - # We execute commands (extracted from `params[:note]`) on the noteable # **before** we save the note because if the note consists of commands # only, there is no need be create a note! @@ -48,7 +40,7 @@ module Notes note.errors.add(:commands_only, 'Commands applied') end - note.commands_changes = command_params.keys + note.commands_changes = command_params end note diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb index 56913568cae..ad1e6f6774a 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/slash_commands_service.rb @@ -3,7 +3,7 @@ module Notes UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, 'MergeRequest' => MergeRequests::UpdateService - } + }.freeze def self.noteable_update_service(note) UPDATE_SERVICES[note.noteable_type] diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb new file mode 100644 index 00000000000..44ae23fad18 --- /dev/null +++ b/app/services/notification_recipient_service.rb @@ -0,0 +1,293 @@ +# +# Used by NotificationService to determine who should receive notification +# +class NotificationRecipientService + attr_reader :project + + def initialize(project) + @project = project + end + + def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true) + custom_action = build_custom_key(action, target) + + recipients = target.participants(current_user) + + unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + recipients = add_project_watchers(recipients) + end + + recipients = add_custom_notifications(recipients, custom_action) + recipients = reject_mention_users(recipients) + + # Re-assign is considered as a mention of the new assignee so we add the + # new assignee to the list of recipients after we rejected users with + # the "on mention" notification level + if [:reassign_merge_request, :reassign_issue].include?(custom_action) + recipients << previous_assignee if previous_assignee + recipients << target.assignee + end + + recipients = reject_muted_users(recipients) + recipients = add_subscribed_users(recipients, target) + + if [:new_issue, :new_merge_request].include?(custom_action) + recipients = add_labels_subscribers(recipients, target) + end + + recipients = reject_unsubscribed_users(recipients, target) + recipients = reject_users_without_access(recipients, target) + + recipients.delete(current_user) if skip_current_user + + recipients.uniq + end + + def build_relabeled_recipients(target, current_user, labels:) + recipients = add_labels_subscribers([], target, labels: labels) + recipients = reject_unsubscribed_users(recipients, target) + recipients = reject_users_without_access(recipients, target) + recipients.delete(current_user) + recipients.uniq + end + + def build_new_note_recipients(note) + target = note.noteable + + ability, subject = if note.for_personal_snippet? + [:read_personal_snippet, note.noteable] + else + [:read_project, note.project] + end + + mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } + + # Add all users participating in the thread (author, assignee, comment authors) + recipients = + if target.respond_to?(:participants) + target.participants(note.author) + else + mentioned_users + end + + unless note.for_personal_snippet? + # Merge project watchers + recipients = add_project_watchers(recipients) + + # Merge project with custom notification + recipients = add_custom_notifications(recipients, :new_note) + end + + # Reject users with Mention notification level, except those mentioned in _this_ note. + recipients = reject_mention_users(recipients - mentioned_users) + recipients = recipients + mentioned_users + + recipients = reject_muted_users(recipients) + + recipients = add_subscribed_users(recipients, note.noteable) + recipients = reject_unsubscribed_users(recipients, note.noteable) + recipients = reject_users_without_access(recipients, note.noteable) + + recipients.delete(note.author) + recipients.uniq + end + + # Remove users with disabled notifications from array + # Also remove duplications and nil recipients + def reject_muted_users(users) + reject_users(users, :disabled) + end + + protected + + # Get project/group users with CUSTOM notification level + def add_custom_notifications(recipients, action) + user_ids = [] + + # Users with a notification setting on group or project + user_ids += user_ids_notifiable_on(project, :custom, action) + user_ids += user_ids_notifiable_on(project.group, :custom, action) + + # Users with global level custom + user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) + + global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) + user_ids += user_ids_with_global_level_custom(global_users_ids, action) + + recipients.concat(User.find(user_ids)) + end + + def add_project_watchers(recipients) + recipients.concat(project_watchers).compact + end + + # Get project users with WATCH notification level + def project_watchers + project_members_ids = user_ids_notifiable_on(project) + + user_ids_with_project_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_global = user_ids_notifiable_on(project.group, :global) + + user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) + + user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) + user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) + + User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a + end + + # Remove users with notification level 'Mentioned' + def reject_mention_users(users) + reject_users(users, :mention) + end + + def add_subscribed_users(recipients, target) + return recipients unless target.respond_to? :subscribers + + recipients + target.subscribers(project) + end + + def user_ids_notifiable_on(resource, notification_level = nil, action = nil) + return [] unless resource + + if notification_level + settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) + settings = settings.select { |setting| setting.events[action] } if action.present? + settings.map(&:user_id) + else + resource.notification_settings.pluck(:user_id) + end + end + + # Build a list of user_ids based on project notification settings + def select_project_members_ids(project, global_setting, user_ids_global_level_watch) + user_ids = user_ids_notifiable_on(project, :watch) + + # If project setting is global, add to watch list if global setting is watch + global_setting.each do |user_id| + if user_ids_global_level_watch.include?(user_id) + user_ids << user_id + end + end + + user_ids + end + + # Build a list of user_ids based on group notification settings + def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) + uids = user_ids_notifiable_on(group, :watch) + + # Group setting is watch, add to user_ids list if user is not project member + user_ids = [] + uids.each do |user_id| + if project_members.exclude?(user_id) + user_ids << user_id + end + end + + # Group setting is global, add to user_ids list if global setting is watch + global_setting.each do |user_id| + if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id) + user_ids << user_id + end + end + + user_ids + end + + def user_ids_with_global_level_watch(ids) + settings_with_global_level_of(:watch, ids).pluck(:user_id) + end + + def user_ids_with_global_level_custom(ids, action) + settings = settings_with_global_level_of(:custom, ids) + settings = settings.select { |setting| setting.events[action] } + settings.map(&:user_id) + end + + def settings_with_global_level_of(level, ids) + NotificationSetting.where( + user_id: ids, + source_type: nil, + level: NotificationSetting.levels[level] + ) + end + + # Reject users which has certain notification level + # + # Example: + # reject_users(users, :watch, project) + # + def reject_users(users, level) + level = level.to_s + + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end + + users = users.to_a.compact.uniq + users = users.select { |u| u.can?(:receive_notifications) } + + users.reject do |user| + global_notification_setting = user.global_notification_setting + + next global_notification_setting.level == level unless project + + setting = user.notification_settings_for(project) + + if project.group && (setting.nil? || setting.global?) + setting = user.notification_settings_for(project.group) + end + + # reject users who globally set mention notification and has no setting per project/group + next global_notification_setting.level == level unless setting + + # reject users who set mention notification in project + next true if setting.level == level + + # reject users who have mention level in project and disabled in global settings + setting.global? && global_notification_setting.level == level + end + end + + def reject_unsubscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions + + recipients.reject do |user| + subscription = target.subscriptions.find_by_user_id(user.id) + subscription && !subscription.subscribed + end + end + + def reject_users_without_access(recipients, target) + ability = case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end + + return recipients unless ability + + recipients.select do |user| + user.can?(ability, target) + end + end + + def add_labels_subscribers(recipients, target, labels: nil) + return recipients unless target.respond_to? :labels + + (labels || target.labels).each do |label| + recipients += label.subscribers(project) + end + + recipients + end + + # Build event key to search on custom notification level + # Check NotificationSetting::EMAIL_EVENTS + def build_custom_key(action, object) + "#{action}_#{object.class.model_name.name.underscore}".to_sym + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 3734e3c4253..f9aa2346759 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -135,7 +135,7 @@ class NotificationService merge_request.target_project, current_user, :merged_merge_request_email, - skip_current_user: !merge_request.merge_when_build_succeeds? + skip_current_user: !merge_request.merge_when_pipeline_succeeds? ) end @@ -150,7 +150,10 @@ class NotificationService end def resolve_all_discussions(merge_request, current_user) - recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions") + recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients( + merge_request, + current_user, + action: "resolve_all_discussions") recipients.each do |recipient| mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later @@ -164,64 +167,15 @@ class NotificationService end # Notify users on new note in system - # - # TODO: split on methods and refactor - # def new_note(note) return true unless note.noteable_type.present? # ignore gitlab service messages return true if note.cross_reference? && note.system? - target = note.noteable - - recipients = [] - - mentioned_users = note.mentioned_users - - ability, subject = if note.for_personal_snippet? - [:read_personal_snippet, note.noteable] - else - [:read_project, note.project] - end - - mentioned_users.select! do |user| - user.can?(ability, subject) - end - - # Add all users participating in the thread (author, assignee, comment authors) - participants = - if target.respond_to?(:participants) - target.participants(note.author) - else - mentioned_users - end - - recipients = recipients.concat(participants) - - unless note.for_personal_snippet? - # Merge project watchers - recipients = add_project_watchers(recipients, note.project) - - # Merge project with custom notification - recipients = add_custom_notifications(recipients, note.project, :new_note) - end - - # Reject users with Mention notification level, except those mentioned in _this_ note. - recipients = reject_mention_users(recipients - mentioned_users, note.project) - recipients = recipients + mentioned_users - - recipients = reject_muted_users(recipients, note.project) - - recipients = add_subscribed_users(recipients, note.project, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - recipients = reject_users_without_access(recipients, note.noteable) - - recipients.delete(note.author) unless note.author.notified_of_own_activity? - recipients = recipients.uniq - notify_method = "note_#{note.to_ability_name}_email".to_sym + recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note) recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end @@ -290,7 +244,7 @@ class NotificationService def project_was_moved(project, old_path_with_namespace) recipients = project.team.members - recipients = reject_muted_users(recipients, project) + recipients = NotificationRecipientService.new(project).reject_muted_users(recipients) recipients.each do |recipient| mailer.project_was_moved_email( @@ -302,7 +256,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = build_recipients(issue, issue.project, current_user) + recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user) recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) @@ -324,12 +278,10 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= build_recipients( + recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( pipeline, - pipeline.project, - pipeline.user, - action: pipeline.status, - skip_current_user: false).map(&:notification_email) + nil, # The acting user, who won't be added to recipients + action: pipeline.status).map(&:notification_email) if recipients.any? mailer.public_send(email_template, pipeline, recipients).deliver_later @@ -338,199 +290,8 @@ class NotificationService protected - # Get project/group users with CUSTOM notification level - def add_custom_notifications(recipients, project, action) - user_ids = [] - - # Users with a notification setting on group or project - user_ids += notification_settings_for(project, :custom, action) - user_ids += notification_settings_for(project.group, :custom, action) - - # Users with global level custom - users_with_project_level_global = notification_settings_for(project, :global) - users_with_group_level_global = notification_settings_for(project.group, :global) - - global_users_ids = users_with_project_level_global.concat(users_with_group_level_global) - user_ids += users_with_global_level_custom(global_users_ids, action) - - recipients.concat(User.find(user_ids)) - end - - # Get project users with WATCH notification level - def project_watchers(project) - project_members = notification_settings_for(project) - - users_with_project_level_global = notification_settings_for(project, :global) - users_with_group_level_global = notification_settings_for(project.group, :global) - - users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) - - users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) - users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users) - - User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a - end - - def notification_settings_for(resource, notification_level = nil, action = nil) - return [] unless resource - - if notification_level - settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.events[action] } if action.present? - settings.map(&:user_id) - else - resource.notification_settings.pluck(:user_id) - end - end - - def users_with_global_level_watch(ids) - settings_with_global_level_of(:watch, ids).pluck(:user_id) - end - - def users_with_global_level_custom(ids, action) - settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.events[action] } - settings.map(&:user_id) - end - - def settings_with_global_level_of(level, ids) - NotificationSetting.where( - user_id: ids, - source_type: nil, - level: NotificationSetting.levels[level] - ) - end - - # Build a list of users based on project notification settings - def select_project_member_setting(project, global_setting, users_global_level_watch) - users = notification_settings_for(project, :watch) - - # If project setting is global, add to watch list if global setting is watch - global_setting.each do |user_id| - if users_global_level_watch.include?(user_id) - users << user_id - end - end - - users - end - - # Build a list of users based on group notification settings - def select_group_member_setting(group, project_members, global_setting, users_global_level_watch) - uids = notification_settings_for(group, :watch) - - # Group setting is watch, add to users list if user is not project member - users = [] - uids.each do |user_id| - if project_members.exclude?(user_id) - users << user_id - end - end - - # Group setting is global, add to users list if global setting is watch - global_setting.each do |user_id| - if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id) - users << user_id - end - end - - users - end - - def add_project_watchers(recipients, project) - recipients.concat(project_watchers(project)).compact - end - - # Remove users with disabled notifications from array - # Also remove duplications and nil recipients - def reject_muted_users(users, project = nil) - reject_users(users, :disabled, project) - end - - # Remove users with notification level 'Mentioned' - def reject_mention_users(users, project = nil) - reject_users(users, :mention, project) - end - - # Reject users which has certain notification level - # - # Example: - # reject_users(users, :watch, project) - # - def reject_users(users, level, project = nil) - level = level.to_s - - unless NotificationSetting.levels.keys.include?(level) - raise 'Invalid notification level' - end - - users = users.to_a.compact.uniq - users = users.reject(&:blocked?) - - users.reject do |user| - global_notification_setting = user.global_notification_setting - - next global_notification_setting.level == level unless project - - setting = user.notification_settings_for(project) - - if project.group && (setting.nil? || setting.global?) - setting = user.notification_settings_for(project.group) - end - - # reject users who globally set mention notification and has no setting per project/group - next global_notification_setting.level == level unless setting - - # reject users who set mention notification in project - next true if setting.level == level - - # reject users who have mention level in project and disabled in global settings - setting.global? && global_notification_setting.level == level - end - end - - def reject_unsubscribed_users(recipients, target) - return recipients unless target.respond_to? :subscriptions - - recipients.reject do |user| - subscription = target.subscriptions.find_by_user_id(user.id) - subscription && !subscription.subscribed - end - end - - def reject_users_without_access(recipients, target) - ability = case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end - - return recipients unless ability - - recipients.select do |user| - user.can?(ability, target) - end - end - - def add_subscribed_users(recipients, project, target) - return recipients unless target.respond_to? :subscribers - - recipients + target.subscribers(project) - end - - def add_labels_subscribers(recipients, project, target, labels: nil) - return recipients unless target.respond_to? :labels - - (labels || target.labels).each do |label| - recipients += label.subscribers(project) - end - - recipients - end - def new_resource_email(target, project, method) - recipients = build_recipients(target, project, target.author, action: "new") + recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new") recipients.each do |recipient| mailer.send(method, recipient.id, target.id).deliver_later @@ -538,7 +299,7 @@ class NotificationService end def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method) - recipients = build_recipients(target, project, current_user, action: "new") + recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new") recipients = recipients & new_mentioned_users recipients.each do |recipient| @@ -549,9 +310,8 @@ class NotificationService def close_resource_email(target, project, current_user, method, skip_current_user: true) action = method == :merged_merge_request_email ? "merge" : "close" - recipients = build_recipients( + recipients = NotificationRecipientService.new(project).build_recipients( target, - project, current_user, action: action, skip_current_user: skip_current_user @@ -566,7 +326,12 @@ class NotificationService previous_assignee_id = previous_record(target, 'assignee_id') previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee) + recipients = NotificationRecipientService.new(project).build_recipients( + target, + current_user, + action: "reassign", + previous_assignee: previous_assignee + ) recipients.each do |recipient| mailer.send( @@ -580,7 +345,7 @@ class NotificationService end def relabeled_resource_email(target, project, labels, current_user, method) - recipients = build_relabeled_recipients(target, project, current_user, labels: labels) + recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels) label_names = labels.map(&:name) recipients.each do |recipient| @@ -589,58 +354,13 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = build_recipients(target, project, current_user, action: "reopen") + recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen") recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later end end - def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true) - custom_action = build_custom_key(action, target) - - recipients = target.participants(current_user) - - unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - recipients = add_project_watchers(recipients, project) - end - - recipients = add_custom_notifications(recipients, project, custom_action) - recipients = reject_mention_users(recipients, project) - - recipients = recipients.uniq - - # Re-assign is considered as a mention of the new assignee so we add the - # new assignee to the list of recipients after we rejected users with - # the "on mention" notification level - if [:reassign_merge_request, :reassign_issue].include?(custom_action) - recipients << previous_assignee if previous_assignee - recipients << target.assignee - end - - recipients = reject_muted_users(recipients, project) - recipients = add_subscribed_users(recipients, project, target) - - if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients, project, target) - end - - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) - - recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? - - recipients.uniq - end - - def build_relabeled_recipients(target, project, current_user, labels:) - recipients = add_labels_subscribers([], project, target, labels: labels) - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) unless current_user.notified_of_own_activity? - recipients.uniq - end - def mailer Notify end @@ -652,10 +372,4 @@ class NotificationService end end end - - # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS - def build_custom_key(action, object) - "#{action}_#{object.class.model_name.name.underscore}".to_sym - end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6dc3d8c2416..fbdaa455651 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -12,7 +12,7 @@ module Projects @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) deny_visibility_level(@project) return @project end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index ba410b79e8c..4e1964f79dd 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -2,9 +2,9 @@ module Projects class DestroyService < BaseService include Gitlab::ShellAdapter - class DestroyError < StandardError; end + DestroyError = Class.new(StandardError) - DELETED_FLAG = '+deleted' + DELETED_FLAG = '+deleted'.freeze def async_execute project.transaction do diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb index f06a3d44c17..604747e39d0 100644 --- a/app/services/projects/download_service.rb +++ b/app/services/projects/download_service.rb @@ -2,7 +2,7 @@ module Projects class DownloadService < BaseService WHITELIST = [ /^[^.]+\.fogbugz.com$/ - ] + ].freeze def initialize(project, url) @project, @url = project, url @@ -25,7 +25,7 @@ module Projects end def http?(url) - url =~ /\A#{URI::regexp(['http', 'https'])}\z/ + url =~ /\A#{URI.regexp(%w(http https))}\z/ end def valid_domain?(url) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index cd230528743..d484a96f785 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -2,7 +2,7 @@ module Projects class ImportService < BaseService include Gitlab::ShellAdapter - class Error < StandardError; end + Error = Class.new(StandardError) def execute add_repository_to_project unless project.gitlab_project_import? @@ -33,6 +33,7 @@ module Projects def import_repository begin + raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url) gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) rescue => e # Expire cache to prevent scenarios such as: @@ -40,7 +41,7 @@ module Projects # 2. Retried import, repo is broken or not imported but +exists?+ still returns true project.repository.before_import if project.repository_exists? - raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 3e241b9e7c0..6d9e7de4f24 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -9,7 +9,7 @@ module Projects class TransferService < BaseService include Gitlab::ShellAdapter - class TransferError < StandardError; end + TransferError = Class.new(StandardError) def execute(new_namespace) if allowed_transfer?(current_user, project, new_namespace) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index f5f9ee88912..523b9f41916 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -2,7 +2,7 @@ module Projects class UpdatePagesService < BaseService BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte - SITE_PATH = 'public/' + SITE_PATH = 'public/'.freeze attr_reader :build @@ -34,6 +34,8 @@ module Projects end rescue => e error(e.message) + ensure + build.erase_artifacts! unless build.has_expiring_artifacts? end private diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb index 050cb3b738b..bdb0e0cc8bf 100644 --- a/app/services/protected_branches/api_update_service.rb +++ b/app/services/protected_branches/api_update_service.rb @@ -15,16 +15,16 @@ module ProtectedBranches case @developers_can_push when true - params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false - params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }] end case @developers_can_merge when true - params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false - params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }] end service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 3e0a85cf059..595653ea58a 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -59,7 +59,7 @@ module SlashCommands @updates[:state_event] = 'reopen' end - desc 'Merge (when build succeeds)' + desc 'Merge (when the pipeline succeeds)' condition do last_diff_sha = params && params[:merge_request_diff_head_sha] issuable.is_a?(MergeRequest) && @@ -255,6 +255,18 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end + desc 'Toggle emoji reward' + params ':emoji:' + condition do + issuable.persisted? + end + command :award do |emoji| + name = award_emoji_name(emoji) + if name && issuable.user_can_award?(current_user, name) + @updates[:emoji_award] = name + end + end + desc 'Set time estimate' params '<1w 3d 2h 14m>' condition do @@ -329,5 +341,10 @@ module SlashCommands ext.references(type) end + + def award_emoji_name(emoji) + match = emoji.match(Banzai::Filter::EmojiFilter.emoji_pattern) + match[1] if match + end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a2bfa422c9d..af0ddbe5934 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -24,21 +24,16 @@ class SystemHooksService key: model.key, id: model.id ) + if model.user - data.merge!( - username: model.user.username - ) + data[:username] = model.user.username end when Project data.merge!(project_data(model)) if event == :rename || event == :transfer - data.merge!({ - old_path_with_namespace: model.old_path_with_namespace - }) + data[:old_path_with_namespace] = model.old_path_with_namespace end - - data when User data.merge!({ name: model.name, @@ -61,6 +56,8 @@ class SystemHooksService when GroupMember data.merge!(group_member_data(model)) end + + data end def build_event_name(model, event) @@ -86,7 +83,7 @@ class SystemHooksService project_id: model.id, owner_name: owner.name, owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase + project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase } end @@ -103,7 +100,7 @@ class SystemHooksService user_email: model.user.email, user_id: model.user.id, access_level: model.human_access, - project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase + project_visibility: Project.visibility_levels.key(project.visibility_level_value).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 87ba72cf991..8e02fe3741a 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -187,14 +187,14 @@ module SystemNoteService end # Called when 'merge when pipeline succeeds' is executed - def merge_when_build_succeeds(noteable, project, author, last_commit) + def merge_when_pipeline_succeeds(noteable, project, author, last_commit) body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end # Called when 'merge when pipeline succeeds' is canceled - def cancel_merge_when_build_succeeds(noteable, project, author) + def cancel_merge_when_pipeline_succeeds(noteable, project, author) body = 'canceled the automatic merge' create_note(noteable: noteable, project: project, author: author, note: body) @@ -356,10 +356,10 @@ module SystemNoteService note: cross_reference_note_content(gfm_reference) } - if noteable.kind_of?(Commit) + if noteable.is_a?(Commit) note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) else - note_options.merge!(noteable: noteable) + note_options[:noteable] = noteable end if noteable.is_a?(ExternalIssue) @@ -385,7 +385,6 @@ module SystemNoteService # Returns Boolean def cross_reference_disallowed?(noteable, mentioner) return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? - return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?)) return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) @@ -408,12 +407,13 @@ module SystemNoteService # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) - if noteable.is_a?(Commit) - # Commits have non-integer IDs, so they're stored in `commit_id` - notes = notes.where(commit_id: noteable.id) - else - notes = notes.where(noteable_id: noteable.id) - end + notes = + if noteable.is_a?(Commit) + # Commits have non-integer IDs, so they're stored in `commit_id` + notes.where(commit_id: noteable.id) + else + notes.where(noteable_id: noteable.id) + end notes_for_mentioner(mentioner, noteable, notes).exists? end diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index 910b4f5e361..a368f4f5b61 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -21,6 +21,8 @@ module Tags else error('Failed to remove tag') end + rescue GitHooksService::PreReceiveError => ex + error(ex.message) end def error(message, return_code = 400) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index ad86b4f9f42..bf7e76ec59e 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -103,7 +103,7 @@ class TodoService # def merge_request_build_failed(merge_request) create_build_failed_todo(merge_request, merge_request.author) - create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When a new commit is pushed to a merge request we should: @@ -121,7 +121,7 @@ class TodoService # def merge_request_build_retried(merge_request) mark_pending_todos_as_done(merge_request, merge_request.author) - mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When a merge request could not be automatically merged due to its unmergeable state we should: @@ -129,7 +129,7 @@ class TodoService # * create a todo for a merge_user # def merge_request_became_unmergeable(merge_request) - create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end # When create a note we should: @@ -201,10 +201,12 @@ class TodoService def update_todos_state_by_ids(ids, current_user, state) todos = current_user.todos.where(id: ids) - # Only return those that are not really on that state - marked_todos = todos.where.not(state: state).update_all(state: state) + # Only update those that are not really on that state + todos = todos.where.not(state: state) + todos_ids = todos.pluck(:id) + todos.update_all(state: state) current_user.update_todos_count_cache - marked_todos + todos_ids end def create_todos(users, attributes) diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index bc0653cb634..833da5bc5d1 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -7,7 +7,7 @@ module Users end def execute(user, options = {}) - unless current_user.admin? || current_user == user + unless Ability.allowed?(current_user, :destroy_user, user) raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" end @@ -26,6 +26,8 @@ module Users ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end + move_issues_to_ghost_user(user) + # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing namespace = user.namespace user_data = user.destroy @@ -33,5 +35,22 @@ module Users user_data end + + private + + def move_issues_to_ghost_user(user) + # Block the user before moving issues to prevent a data race. + # If the user creates an issue after `move_issues_to_ghost_user` + # runs and before the user is destroyed, the destroy will fail with + # an exception. We block the user so that issues can't be created + # after `move_issues_to_ghost_user` runs and before the destroy happens. + user.block + + ghost_user = User.ghost + + user.issues.update_all(author_id: ghost_user.id) + + user.reload + end end end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index fad741531ea..8f6f5b937c4 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -93,9 +93,7 @@ module Users end def current_authorizations_per_project - current_authorizations.each_with_object({}) do |row, hash| - hash[row.project_id] = row - end + current_authorizations.index_by(&:project_id) end def current_authorizations @@ -115,11 +113,23 @@ module Users # Returns a union query of projects that the user is authorized to access def project_authorizations_union relations = [ + # Personal projects user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - user.groups_projects.select_for_project_authorization, + + # Projects the user is a member of user.projects.select_for_project_authorization, + + # Projects of groups the user is a member of + user.groups_projects.select_for_project_authorization, + + # Projects of subgroups of groups the user is a member of + user.nested_groups_projects.select_for_project_authorization, + + # Projects shared with groups the user is a member of user.groups.joins(:shared_projects).select_for_project_authorization, - user.nested_projects.select_for_project_authorization + + # Projects shared with subgroups of groups the user is a member of + user.nested_groups.joins(:shared_projects).select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 86f317dcd18..e84944ed411 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader File.join(self.class.artifacts_cache_path, @build.artifacts_path) end - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def filename file.try(:filename) end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index cfcb877cc3e..109eb2fea0b 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,9 +1,10 @@ class AttachmentUploader < GitlabUploader + include RecordsUploads include UploaderHelper storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 265cea2d2c6..66d3bcb998a 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,10 +1,11 @@ class AvatarUploader < GitlabUploader + include RecordsUploads include UploaderHelper storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def exists? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 23b7318827c..d6ccf0dc92c 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,30 +1,53 @@ class FileUploader < GitlabUploader + include RecordsUploads include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} storage :file - attr_accessor :project, :secret + def self.absolute_path(upload_record) + File.join( + self.dynamic_path_segment(upload_record.model), + upload_record.path + ) + end - def initialize(project, secret = nil) - @project = project - @secret = secret || self.class.generate_secret + # Returns the part of `store_dir` that can change based on the model's current + # path + # + # This is used to build Upload paths dynamically based on the model's current + # namespace and path, allowing us to ignore renames or transfers. + # + # model - Object that responds to `path_with_namespace` + # + # Returns a String without a trailing slash + def self.dynamic_path_segment(model) + File.join(CarrierWave.root, base_dir, model.path_with_namespace) end - def base_dir - "uploads" + attr_accessor :project + attr_reader :secret + + def initialize(project, secret = nil) + @project = project + @secret = secret || generate_secret end def store_dir - File.join(base_dir, @project.path_with_namespace, @secret) + File.join(dynamic_path_segment, @secret) end def cache_dir File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def secure_url - File.join("/uploads", @secret, file.filename) + def model + project + end + + def relative_path + self.file.path.sub("#{dynamic_path_segment}/", '') end def to_markdown @@ -35,17 +58,27 @@ class FileUploader < GitlabUploader filename = image_or_video? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") - markdown = "[#{escaped_filename}](#{self.secure_url})" + markdown = "[#{escaped_filename}](#{secure_url})" markdown.prepend("!") if image_or_video? || dangerous? { alt: filename, - url: self.secure_url, + url: secure_url, markdown: markdown } end - def self.generate_secret + private + + def dynamic_path_segment + self.class.dynamic_path_segment(model) + end + + def generate_secret SecureRandom.hex end + + def secure_url + File.join('/uploads', @secret, file.filename) + end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 02d7c601d6c..d662ba6820c 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -1,4 +1,18 @@ class GitlabUploader < CarrierWave::Uploader::Base + def self.absolute_path(upload_record) + File.join(CarrierWave.root, upload_record.path) + end + + def self.base_dir + 'uploads' + end + + delegate :base_dir, to: :class + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + # Reduce disk IO def move_to_cache true @@ -8,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base def move_to_store true end + + # Designed to be overridden by child uploaders that have a dynamic path + # segment -- that is, a path that changes based on mutable attributes of its + # associated model + # + # For example, `FileUploader` builds the storage path based on the associated + # project model's `path_with_namespace` value, which can change when the + # project or its containing namespace is moved or renamed. + def relative_path + self.file.path.sub("#{root}/", '') + end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb new file mode 100644 index 00000000000..4c127f29250 --- /dev/null +++ b/app/uploaders/records_uploads.rb @@ -0,0 +1,34 @@ +module RecordsUploads + extend ActiveSupport::Concern + + included do + after :store, :record_upload + before :remove, :destroy_upload + end + + private + + # After storing an attachment, create a corresponding Upload record + # + # NOTE: We're ignoring the argument passed to this callback because we want + # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the + # `Tempfile` object the callback gets. + # + # Called `after :store` + def record_upload(_tempfile) + return unless file_storage? + return unless file.exists? + + Upload.record(self) + end + + # Before removing an attachment, destroy any Upload records at the same path + # + # Called `before :remove` + def destroy_upload(*args) + return unless file_storage? + return unless file + + Upload.remove_path(relative_path) + end +end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 35fd1ed23f8..7635c20ab3a 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,15 +1,15 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play # on IE >= 9. # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html - VIDEO_EXT = %w[mp4 m4v mov webm ogv] + VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze # These extension types can contain dangerous code and should only be embedded inline with # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". - DANGEROUS_EXT = %w[svg] + DANGEROUS_EXT = %w[svg].freeze def image? extension_match?(IMAGE_EXT) @@ -27,6 +27,8 @@ module UploaderHelper extension_match?(DANGEROUS_EXT) end + private + def extension_match?(extensions) return false unless file @@ -40,8 +42,4 @@ module UploaderHelper extensions.include?(extension.downcase) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb index 09bfa613cbe..94542125d43 100644 --- a/app/validators/addressable_url_validator.rb +++ b/app/validators/addressable_url_validator.rb @@ -18,7 +18,7 @@ # end # class AddressableUrlValidator < ActiveModel::EachValidator - DEFAULT_OPTIONS = { protocols: %w(http https ssh git) } + DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze def validate_each(record, attribute, value) unless valid_url?(value) diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb new file mode 100644 index 00000000000..10ff44031c6 --- /dev/null +++ b/app/validators/duration_validator.rb @@ -0,0 +1,17 @@ +# DurationValidator +# +# Validate the format conforms with ChronicDuration +# +# Example: +# +# class ApplicationSetting < ActiveRecord::Base +# validates :default_artifacts_expire_in, presence: true, duration: true +# end +# +class DurationValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + ChronicDuration.parse(value) + rescue ChronicDuration::DurationParseError + record.errors.add(attribute, "is not a correct duration") + end +end diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb new file mode 100644 index 00000000000..37a314adee6 --- /dev/null +++ b/app/validators/importable_url_validator.rb @@ -0,0 +1,11 @@ +# ImportableUrlValidator +# +# This validator blocks projects from using dangerous import_urls to help +# protect against Server-side Request Forgery (SSRF). +class ImportableUrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if Gitlab::UrlBlocker.blocked_url?(value) + record.errors.add(attribute, "imports are not allowed from that URL") + end + end +end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index eb3ed31b65b..77ca033e97f 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -35,12 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator users ].freeze + WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree + preview blob blame raw files create_dir find_file + artifacts graphs refs badges].freeze + + STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze + def self.valid?(value) !reserved?(value) && follow_format?(value) end - def self.reserved?(value) - RESERVED.include?(value) + def self.reserved?(value, strict: false) + if strict + STRICT_RESERVED.include?(value) + else + RESERVED.include?(value) + end end def self.follow_format?(value) @@ -54,7 +64,9 @@ class NamespaceValidator < ActiveModel::EachValidator record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) end - if reserved?(value) + strict = record.is_a?(Group) && record.parent_id + + if reserved?(value, strict: strict) record.errors.add(attribute, "#{value} is a reserved name") end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 36279daa743..ee2ae65be7b 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED - - %w[dashboard help ci admin search notes services assets profile public] + - %w[tree commits wikis new edit create update logs_tree - preview blob blame raw files create_dir find_file]).freeze + RESERVED = (NamespaceValidator::STRICT_RESERVED - + %w[dashboard help ci admin search notes services assets profile public]).freeze def self.valid?(value) !reserved?(value) diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 6c48328da4f..6a5e170ddd8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -16,4 +16,4 @@ - else .empty-state .text-center - %h4 There are no abuse reports! #{emoji_icon 'tada'} + %h4 There are no abuse reports! #{emoji_icon('tada')} diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 9175b3d3f96..e403a9da616 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -48,7 +48,7 @@ .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' - if @appearance.persisted? - = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank' + = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer' - if @appearance.updated_at %span.pull-right diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 749c74b8110..3eab065bb9f 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -212,8 +212,16 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each jobs's artifacts can have - = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") + Set the maximum file size for each job's artifacts + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') + .form-group + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :default_artifacts_expire_in, class: 'form-control' + .help-block + Set the default expiration time for each job's artifacts. + 0 for unlimited. + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') - if Gitlab.config.registry.enabled %fieldset @@ -352,6 +360,29 @@ Generate API key at %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :unique_ips_limit_enabled do + = f.check_box :unique_ips_limit_enabled + Limit sign in from multiple ips + %span.help-block#unique_ip_help_block + Helps prevent malicious users hide their activity + + .form-group + = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :unique_ips_limit_per_user, class: 'form-control' + .help-block + Maximum number of unique IPs per user + + .form-group + = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :unique_ips_limit_time_window, class: 'form-control' + .help-block + How many seconds an IP will be counted towards the limit + %fieldset %legend Abuse reports .form-group @@ -373,7 +404,7 @@ Enable Sentry .help-block Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com + %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com .form-group = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index c689b26d6e6..061f8991b11 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -26,4 +26,4 @@ .form-actions = f.submit 'Submit', class: "btn btn-save wide" - = link_to "Cancel", admin_applications_path, class: "btn btn-default" + = link_to "Cancel", admin_applications_path, class: "btn btn-cancel" diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml new file mode 100644 index 00000000000..1378dde52ab --- /dev/null +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -0,0 +1,8 @@ +- page_title "Impersonation Tokens", @user.name, "Users" += render 'admin/users/head' + +.row.prepend-top-default + .col-lg-12 + = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes + + = render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml new file mode 100644 index 00000000000..c1a9f8d6ddd --- /dev/null +++ b/app/views/admin/projects/_projects.html.haml @@ -0,0 +1,32 @@ +.js-projects-list-holder + - if @projects.any? + %ul.projects-list.content-list + - @projects.each_with_index do |project| + %li.project-row + .controls + - if project.archived + %span.label.label-warning archived + %span.badge + = storage_counter(project.statistics.storage_size) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" + = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .title + = link_to [:admin, project.namespace.becomes(Namespace), project] do + .dash-project-avatar + .avatar-container.s40 + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + + - if project.description.present? + .description + = markdown_field(project, :description) + + = paginate @projects, theme: 'gitlab' + - else + .nothing-here-block No projects found diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index c35945c5a35..3301f55b8a8 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -7,40 +7,24 @@ %div{ class: container_class } .top-area .prepend-top-default - = form_tag admin_projects_path, method: :get do |f| - .search-holder - .search-field-holder - = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' - - - if params[:visibility_level].present? - = hidden_field_tag 'visibility_level', params[:visibility_level] - - - if params[:sort].present? - = hidden_field_tag 'sort', params[:sort] - - - if params[:personal].present? - = hidden_field_tag 'visibility_level', 'true' - - - if params[:archived].present? - = hidden_field_tag 'archived', 'true' - - = icon("search", class: "search-icon") - - .dropdown - - toggle_text = 'Namespace' - - if params[:namespace_id].present? - - namespace = Namespace.find(params[:namespace_id]) - - toggle_text = "#{namespace.kind}: #{namespace.full_path}" - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) - .dropdown-menu.dropdown-select.dropdown-menu-align-right - = dropdown_title('Namespaces') - = dropdown_filter("Search for Namespace") - = dropdown_content - = dropdown_loading - = render 'shared/projects/dropdown' - = link_to new_project_path, class: 'btn btn-new' do - New Project - = button_tag "Search", class: "btn btn-primary btn-search hide" + .search-holder + = render 'shared/projects/search_form', autofocus: true, icon: true + .dropdown + - toggle_text = 'Namespace' + - if params[:namespace_id].present? + = hidden_field_tag :namespace_id, params[:namespace_id] + - namespace = Namespace.find(params[:namespace_id]) + - toggle_text = "#{namespace.kind}: #{namespace.full_path}" + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select.dropdown-menu-align-right + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project + = button_tag "Search", class: "btn btn-primary btn-search hide" %ul.nav-links - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } @@ -58,35 +42,4 @@ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public - .projects-list-holder - - if @projects.any? - %ul.projects-list.content-list - - @projects.each_with_index do |project| - %li.project-row - .controls - - if project.archived - %span.label.label-warning archived - %span.badge - = storage_counter(project.statistics.storage_size) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" - = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" - .title - = link_to [:admin, project.namespace.becomes(Namespace), project] do - .dash-project-avatar - .avatar-container.s40 - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - - - if project.description.present? - .description - = markdown_field(project, :description) - - = paginate @projects, theme: 'gitlab' - - else - .nothing-here-block No projects found + = render 'projects' diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index deb62845e1c..d4d166ab7b6 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -15,6 +15,8 @@ %td = runner.description %td + = runner.version + %td - if runner.shared? n/a - else diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index d725e477044..7d26864d0f3 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -67,6 +67,7 @@ %th Type %th Runner token %th Description + %th Version %th Projects %th Jobs %th Tags diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9984e733956..be41c33b853 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -2,11 +2,13 @@ = @user.name - if @user.blocked? %span.cred (Blocked) + - if @user.internal? + %span.cred (Internal) - if @user.admin %span.cred (Admin) .pull-right - - unless @user == current_user || @user.blocked? + - if @user != current_user && @user.can?(:log_in) = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do %i.fa.fa-pencil-square-o @@ -21,4 +23,6 @@ = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do = link_to "Identities", admin_user_identities_path(@user) + = nav_link(controller: :impersonation_tokens) do + = link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user) .append-bottom-default diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 3b5c713ac2d..a756cb7243a 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -34,7 +34,7 @@ - if user.access_locked? %li = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? + - if user.can_be_removed? && can?(current_user, :destroy_user, @user) %li.divider %li = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 76b1291fe10..840d843f069 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -173,7 +173,7 @@ .panel-heading Remove user .panel-body - - if @user.can_be_removed? + - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting a user has the following effects: %ul %li All user content like authored issues, snippets, comments will be removed @@ -189,3 +189,6 @@ %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete this user. + - else + %p + You don't have access to delete this user. diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index e3305e21e96..a1ef34dc588 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -4,7 +4,7 @@ %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_state_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } - = emoji_icon(emoji, sprite: false) + = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index dc76599b776..89d991abe54 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -2,10 +2,9 @@ = render "events/event_last_push", event: @last_push .nav-block - - if current_user - .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do - %i.fa.fa-rss + .controls + = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 23c145ebbb4..13eaba41f4c 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -6,7 +6,9 @@ = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore groups' do Explore Groups - - if current_user.can_create_group? - .nav-controls + .nav-controls + = render 'shared/groups/search_form' + = render 'shared/groups/dropdown' + - if current_user.can_create_group? = link_to new_group_path, class: "btn btn-new" do New Group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 48b0fd504f4..600ee63a5c0 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -13,8 +13,7 @@ Explore projects .nav-controls - = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" + = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index aa57df14c23..190ad4b40a5 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,6 +1,5 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") - page_title "Activity" - header_title "Activity", activity_dashboard_path diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml new file mode 100644 index 00000000000..6c3bf1a2b3b --- /dev/null +++ b/app/views/dashboard/groups/_groups.html.haml @@ -0,0 +1,6 @@ +.js-groups-list-holder + %ul.content-list + - @group_members.each do |group_member| + = render 'shared/groups/group', group: group_member.group, group_member: group_member + + = paginate @group_members, theme: 'gitlab' diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 1a679c51774..73ab2c95ff9 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -5,9 +5,4 @@ - if @group_members.empty? = render 'empty_state' - else - %ul.content-list - - @group_members.each do |group_member| - - group = group_member.group - = render 'shared/groups/group', group: group, group_member: group_member - - = paginate @group_members, theme: 'gitlab' + = render 'groups' diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index bdea1064096..06fb531b546 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 653052f7c54..10867140d4f 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -1,17 +1,13 @@ - page_title "Issues" - header_title "Issues", issues_dashboard_path(assignee_id: current_user.id) = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe + = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do + = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 917bfbd47e9..505b475f55b 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,11 +1,11 @@ -- page_title "Milestones" -- header_title "Milestones", dashboard_milestones_path +- page_title 'Milestones' +- header_title 'Milestones', dashboard_milestones_path .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: @milestone_states .nav-controls - = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true + = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true .milestones %ul.content-list @@ -15,4 +15,4 @@ - else - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: 'gitlab' diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index fb5be63b472..13f7a8ddcec 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Activity" - xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 4f36a4a1c73..eef794dbd51 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,17 +1,17 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") - page_title "Projects" - header_title "Projects", dashboard_projects_path -- if @projects.any? || params[:filter_projects] +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } +- if @projects.any? || params[:name] = render 'dashboard/projects_head' - if @last_push = render "events/event_last_push", event: @last_push -- if @projects.any? || params[:filter_projects] +- if @projects.any? || params[:name] = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 70705923d42..162ae153b1c 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -6,7 +6,7 @@ - if @last_push = render "events/event_last_push", event: @last_push -- if @projects.any? +- if @projects.any? || params[:filter_projects] = render 'projects' - else %h3 You don't have starred projects yet diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index a3993d5ef16..d0c12aa57ae 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -36,9 +36,14 @@ - if todo.pending? .todo-actions - = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do + = link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do Done = icon('spinner spin') - = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do + = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do Undo = icon('spinner spin') + - else + .todo-actions + = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do + Add todo + = icon('spinner spin') diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 16a5713948a..d31ced004a0 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -19,9 +19,12 @@ .nav-controls - if @todos.any?(&:pending?) - = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do Mark all as done = icon('spinner spin') + = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do + Undo mark all as done + = icon('spinner spin') .todos-filters .row-content-block.second-block @@ -46,19 +49,19 @@ = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: 'Action' } }) - .pull-right - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + .filter-item.sort-filter + .dropdown + %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-sort %li - = link_to todos_filter_path(sort: sort_value_priority) do - = sort_title_priority + = link_to todos_filter_path(sort: sort_value_label_priority) do + = sort_title_label_priority = link_to todos_filter_path(sort: sort_value_recently_created) do = sort_title_recently_created = link_to todos_filter_path(sort: sort_value_oldest_created) do @@ -67,12 +70,16 @@ .js-todos-all - if @todos.any? - .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - .panel.panel-default.panel-small.panel-without-border - %ul.content-list.todos-list - = render @todos - = paginate @todos, theme: "gitlab" - + .js-todos-list-container + .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } + .panel.panel-default.panel-small.panel-without-border + %ul.content-list.todos-list + = render @todos + = paginate @todos, theme: "gitlab" + .js-nothing-here-container.todos-all-done.hidden + = render "shared/empty_states/icons/todos_all_done.svg" + %h4.text-center + You're all done! - elsif current_user.todos.any? .todos-all-done = render "shared/empty_states/icons/todos_all_done.svg" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 951f03083bf..a039756c7e2 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,6 +1,6 @@ - if inject_u2f_api? - content_for :page_specific_javascripts do - = page_specific_javascript_tag('u2f.js') + = page_specific_javascript_bundle_tag('u2f') %div = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 5a44ec45b7b..a2f6a7ab1cb 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,11 +4,11 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name + = f.label :name, 'Full name' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group = f.label :username - = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 2deadbeeceb..ee452add394 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -2,5 +2,5 @@ %tr.notes_holder{ class: ('hide' unless expanded) } %td.notes_line{ colspan: 2 } %td.notes_content - .content + .content{ class: ('hide' unless expanded) } = render "discussions/notes", discussion: discussion diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml new file mode 100644 index 00000000000..ca9e0e8728a --- /dev/null +++ b/app/views/discussions/_new_issue_for_all_discussions.html.haml @@ -0,0 +1,6 @@ +- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project) + .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" } + .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue", + "aria-label" => "Resolve all discussions in a new issue", + "data-container" => "body" } + = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion' diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml new file mode 100644 index 00000000000..df5546a1e32 --- /dev/null +++ b/app/views/discussions/_new_issue_for_discussion.html.haml @@ -0,0 +1,8 @@ +- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project) + %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'", + "inline-template" => true } + .btn-group{ role: "group", "v-if" => "showButton" } + .btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue", + "aria-label" => "Resolve this discussion in a new issue", + "data-container" => "body" } + = link_to custom_icon('icon_mr_issue'), new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion' diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index dfdbdf1f969..2789391819c 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -11,6 +11,8 @@ = link_to_reply_discussion(discussion, line_type) = render "discussions/resolve_all", discussion: discussion - if discussion.for_merge_request? - = render "discussions/jump_to_next", discussion: discussion + .btn-group.discussion-actions + = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable + = render "discussions/jump_to_next", discussion: discussion - else = link_to_reply_discussion(discussion) diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index a196561f381..82aa51f9778 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -27,6 +27,7 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Authorize", class: "btn btn-success wide pull-left" = form_tag oauth_authorization_path, method: :delete do = hidden_field_tag :client_id, @pre_auth.client.uid @@ -34,4 +35,5 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml deleted file mode 100644 index 49bd9acd2db..00000000000 --- a/app/views/emojis/index.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -.emoji-menu - = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji" - .emoji-menu-content - - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis| - %h5.emoji-menu-title - = Gitlab::AwardEmoji::CATEGORIES[category] - %ul.clearfix.emoji-menu-list - - emojis.each do |emoji| - %li.pull-left.text-center.emoji-menu-list-item - %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" } - = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder index 7890e717aa7..158061579f6 100644 --- a/app/views/events/_event.atom.builder +++ b/app/views/events/_event.atom.builder @@ -4,12 +4,12 @@ xml.entry do xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link href: event_feed_url(event) xml.title truncate(event_feed_title(event), length: 80) - xml.updated event.created_at.xmlschema + xml.updated event.updated_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.author do xml.name event.author_name - xml.email event.author_email + xml.email event.author_public_email end xml.summary(type: "xhtml") do |summary| diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index f08c96df309..64b5a733b77 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -15,6 +15,6 @@ = link_to note.attachment.url, target: '_blank' do = image_tag note.attachment.url, class: 'note-image-attach' - else - = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do + = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do %i.fa.fa-paperclip = note.attachment_identifier diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml new file mode 100644 index 00000000000..794c6d1d170 --- /dev/null +++ b/app/views/explore/groups/_groups.html.haml @@ -0,0 +1,6 @@ +.js-groups-list-holder + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group + + = paginate @groups, theme: 'gitlab' diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml new file mode 100644 index 00000000000..c8d95b52156 --- /dev/null +++ b/app/views/explore/groups/_nav.html.haml @@ -0,0 +1,8 @@ +.top-area + %ul.nav-links + = nav_link(page: explore_groups_path) do + = link_to explore_groups_path do + Explore Groups + .nav-controls + = render 'shared/groups/search_form' + = render 'shared/groups/dropdown' diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 73cf6e87eb4..bb2cd0d44c8 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -5,41 +5,9 @@ = render 'dashboard/groups_head' - else = render 'explore/head' + = render 'nav' -.row-content-block.clearfix - .pull-left - = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| - = hidden_field_tag :sort, @sort - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false - .form-group - = button_tag 'Search', class: "btn btn-default" - - .pull-right - .dropdown.inline - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to explore_groups_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - -%ul.content-list - - @groups.each do |group| - = render 'shared/groups/group', group: group - - unless @groups.present? - .nothing-here-block No public groups - - -= paginate @groups, theme: "gitlab" +- if @groups.present? + = render 'groups' +- else + .nothing-here-block No public groups diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml index 614b5431779..e0a2a1e9c96 100644 --- a/app/views/explore/projects/_nav.html.haml +++ b/app/views/explore/projects/_nav.html.haml @@ -1,10 +1,17 @@ -%ul.nav-links - = nav_link(page: [trending_explore_projects_path, explore_root_path]) do - = link_to trending_explore_projects_path do - Trending - = nav_link(page: starred_explore_projects_path) do - = link_to starred_explore_projects_path do - Most stars - = nav_link(page: explore_projects_path) do - = link_to explore_projects_path do - All +.top-area + %ul.nav-links + = nav_link(page: [trending_explore_projects_path, explore_root_path]) do + = link_to trending_explore_projects_path do + Trending + = nav_link(page: starred_explore_projects_path) do + = link_to starred_explore_projects_path do + Most stars + = nav_link(page: explore_projects_path) do + = link_to explore_projects_path do + All + + .nav-controls + - unless current_user + = render 'shared/projects/search_form' + = render 'shared/projects/dropdown' + = render 'filter' diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 42b50481b9d..ec461755103 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -6,10 +6,5 @@ - else = render 'explore/head' -.top-area - = render 'explore/projects/nav' - - .nav-controls - = render 'filter' - += render 'explore/projects/nav' = render 'projects', projects: @projects diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index 71cc4d87b1f..d7851c79990 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -2,10 +2,9 @@ = render "events/event_last_push", event: @last_push .nav-block - - if current_user - .controls - = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do - %i.fa.fa-rss + .controls + = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml new file mode 100644 index 00000000000..20de1b4c973 --- /dev/null +++ b/app/views/groups/_create_chat_team.html.haml @@ -0,0 +1,16 @@ +.form-group + = f.label :create_chat_team, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox.js-toggle-container + = f.label :create_chat_team do + .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false) + Create a Mattermost team for this group + %br + %small.light.js-toggle-content + Mattermost URL: + = Settings.mattermost.host + %span> / + %span{ "data-bind-out" => "create_chat_team" } diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml new file mode 100644 index 00000000000..873504099d4 --- /dev/null +++ b/app/views/groups/_head.html.haml @@ -0,0 +1,14 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group Home' do + %span + Home + + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity diff --git a/app/views/groups/_head_issues.html.haml b/app/views/groups/_head_issues.html.haml new file mode 100644 index 00000000000..d554bc23743 --- /dev/null +++ b/app/views/groups/_head_issues.html.haml @@ -0,0 +1,19 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do + %span + List + + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels + + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml new file mode 100644 index 00000000000..2454e7355a7 --- /dev/null +++ b/app/views/groups/_settings_head.html.haml @@ -0,0 +1,14 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'General' do + %span + General + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index aaad265b3ee..3969e56f937 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -1,8 +1,8 @@ = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") -- page_title "Activity" +- page_title "Activity" += render 'groups/head' %section.activities = render 'activities' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2706e8692d1..80a77dab97f 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,3 +1,4 @@ += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading Group settings diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 0cc6466d34e..469768d83f2 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url, rel: "alternate", type: "text/html" xml.id issues_group_url - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 83edb719692..f4c17dc2d16 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,18 +1,17 @@ - page_title "Issues" += render "head_issues" = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") - if group_issues(@group).exists? .top-area = render 'shared/issuable/nav', type: :issues - - if current_user - .nav-controls - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + .nav-controls + = link_to params.merge(rss_url_options), class: 'btn' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 45325d6bc4b..2bc00fb16c8 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,4 +1,5 @@ - page_title 'Labels' += render "groups/head_issues" .top-area.adjust .nav-text diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index cd5388fe402..6893168f039 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,7 +1,8 @@ - page_title "Milestones" += render "groups/head_issues" .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: @milestone_states .nav-controls - if can?(current_user, :admin_milestones, @group) diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 38d63fd9acc..000c7af2326 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,6 +16,8 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 2e7e5e5c309..83bdd654f27 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,4 @@ -- page_title "Projects" += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index b68bf444d27..914091dfd15 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@group.name} activity" - xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index b040f404ac4..18997baa998 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,9 +1,9 @@ - @no_container = true = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") += render 'groups/head' = render 'groups/home_panel' @@ -11,8 +11,7 @@ .top-area = render 'groups/show_nav' .nav-controls - = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false + = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml index 8610ae7e0ef..be809083139 100644 --- a/app/views/groups/subgroups.html.haml +++ b/app/views/groups/subgroups.html.haml @@ -1,5 +1,6 @@ - @no_container = true += render 'head' = render 'groups/home_panel' .groups-header{ class: container_class } diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 705e20112fa..2684f16c373 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -163,7 +163,7 @@ .key g .key g %td - Go to graphs + Go to repository charts %tr %td.shortcut .key g diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 31631887317..f93b6b63426 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -17,7 +17,7 @@ %br Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br - Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. + Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. - if current_application_settings.help_page_text.present? %hr = markdown_field(current_application_settings, :help_page_text) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 87f9b503989..1fb2c6271ad 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -410,7 +410,7 @@ :javascript $('#js-project-dropdown').glDropdown({ data: function (term, callback) { - Api.projects(term, "last_activity_at", function (data) { + Api.projects(term, { order_by: 'last_activity_at' }, function (data) { callback(data); }); }, diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index e18bd47798b..e6058617ac9 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -33,7 +33,7 @@ - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank' + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -50,7 +50,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' %td.import-target %fieldset.row .input-group @@ -70,7 +70,7 @@ - @incompatible_repos.each do |repo| %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank' + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status = label_tag 'Incompatible Project', nil, class: 'label label-danger' diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index d5b88709a34..7456799ca0e 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -43,7 +43,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo["id"]}" } %td - = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer' %td.import-target = import_project_target(repo['namespace']['path'], repo['name']) %td.import-actions.job-status diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 336becd229e..c5800a1cca0 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -13,7 +13,7 @@ %li %p Go to - #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}. + #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}. %li %p Make sure you're logged into the account that owns the projects you'd like to import. diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 5e01af008be..60de6bfe816 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -36,7 +36,7 @@ - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -53,7 +53,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo.id}" } %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer' %td.import-target #{current_user.username}/#{repo.name} %td.import-actions.job-status @@ -63,7 +63,7 @@ - @incompatible_repos.each do |repo| %tr{ id: "repo_#{repo.id}" } %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status = label_tag "Incompatible Project", nil, class: "label label-danger" diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder index 96831874144..23a88448055 100644 --- a/app/views/issues/_issue.atom.builder +++ b/app/views/issues/_issue.atom.builder @@ -2,12 +2,12 @@ xml.entry do xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.xmlschema + xml.updated issue.updated_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do xml.name issue.author_name - xml.email issue.author_email + xml.email issue.author_public_email end xml.summary issue.title @@ -26,7 +26,7 @@ xml.entry do if issue.assignee xml.assignee do xml.name issue.assignee.name - xml.email issue.assignee.email + xml.email issue.assignee_public_email end end end diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml index 65887aacbaf..04e2d4b63e6 100644 --- a/app/views/koding/index.html.haml +++ b/app/views/koding/index.html.haml @@ -2,5 +2,5 @@ %p = icon('circle', class: 'cgreen') Integration is active for - = link_to koding_project_url, target: '_blank' do + = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do #{current_application_settings.koding_url} diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 302c1794628..f6d8bb08a64 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,7 +28,9 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" - = javascript_include_tag(*webpack_asset_paths("application")) + = javascript_include_tag(*webpack_asset_paths("runtime")) + = javascript_include_tag(*webpack_asset_paths("common")) + = javascript_include_tag(*webpack_asset_paths("main")) - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 3daa1e90a8c..769f6fb0151 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -4,7 +4,6 @@ - if project :javascript gl.GfmAutoComplete.dataSources = { - emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}", members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1717ed6b365..a35a918d501 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ .page-with-sidebar{ class: page_gutter_class } - if defined?(nav) && nav .layout-nav - %div{ class: container_class } + .container-fluid = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 19bd9b6d5c9..36543edc040 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data = render "layouts/header/default", title: header_title diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f8986893776..5fde5c2613e 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -19,7 +19,7 @@ %ul.nav.navbar-nav %li.hidden-sm.hidden-xs = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm.visible-xs + %li.visible-sm-inline-block.visible-xs-inline-block = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if current_user @@ -36,6 +36,10 @@ = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_count_format(todos_pending_count) + - if current_user.can_create_project? + %li + = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -51,8 +55,6 @@ = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li = link_to "Settings", profile_path, aria: { label: "Settings" } - %li - = link_to "Help", help_path, aria: { label: "Help" } %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } @@ -65,7 +67,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - %h1.title= title + %h1.title{ class: ('initializing' if @has_group_title) }= title = yield :header_content diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml new file mode 100644 index 00000000000..53268cc22f8 --- /dev/null +++ b/app/views/layouts/mailer.html.haml @@ -0,0 +1,72 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + = header_logo + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + = yield + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml new file mode 100644 index 00000000000..6a9c6ced9cc --- /dev/null +++ b/app/views/layouts/mailer.text.haml @@ -0,0 +1,5 @@ += yield + +You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. +Manage all notifications: #{profile_notifications_url} +Help: #{help_url} diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 19a947af4ca..d068c895fa3 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -33,7 +33,7 @@ Abuse Reports %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - - if askimet_enabled? + - if akismet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path, title: "Spam Logs" do %span diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5d4178f03d7..15285ee32a3 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -24,16 +24,16 @@ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do %span Issues - (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))}) + .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do %span Merge Requests - (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))}) + .badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) = nav_link(controller: 'dashboard/snippets') do = link_to dashboard_snippets_path, title: 'Snippets' do %span Snippets %li.divider %li - = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab' + = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f3539fd372d..8605380848d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,27 +1,14 @@ -= render 'layouts/nav/group_settings' .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Home' do %span Group - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - = nav_link(controller: [:group, :labels]) do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones - = nav_link(path: 'groups#issues') do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do = link_to issues_group_path(@group), title: 'Issues' do %span Issues @@ -33,7 +20,12 @@ Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(controller: [:group_members]) do + = nav_link(path: 'group_members#index') do = link_to group_group_members_path(@group), title: 'Members' do %span Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to edit_group_path(@group), title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml deleted file mode 100644 index 30feb6813b4..00000000000 --- a/app/views/layouts/nav/_group_settings.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if current_user - - can_admin_group = can?(current_user, :admin_group, @group) - - can_edit = can?(current_user, :admin_group, @group) - - - if can_admin_group || can_edit - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - if can_admin_group - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if can_edit && can_admin_group - %li.divider - %li - = link_to 'Edit Group', edit_group_path(@group) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 7883823b21e..299dace3406 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,60 +1,27 @@ -- if current_user - .controls - .dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - can_edit = can?(current_user, :admin_project, @project) - - = render 'layouts/nav/project_settings', can_edit: can_edit - - - if can_edit - %li.divider - %li - = link_to edit_project_path(@project) do - Edit Project - +- can_edit = can?(current_user, :admin_project, @project) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases graphs network)) do = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do %span Repository - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do %span Registry - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - %span - Graphs - - if project_nav_tab? :issues = nav_link(controller: [:issues, :labels, :milestones, :boards]) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do @@ -70,6 +37,12 @@ Merge Requests %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :environments]) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do @@ -82,18 +55,41 @@ %span Snippets - -# Global shortcut to network page for compatibility + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + - else + = nav_link(path: %w[members#show]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + + -# Shortcut to Project > Activity + %li.hidden + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) - if project_nav_tab? :network %li.hidden = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts - -# Shortcut to create a new issue + -# Shortcut to Issues > New Issue %li.hidden = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do Create a new issue - -# Shortcut to builds page + -# Shortcut to Pipelines > Jobs - if project_nav_tab? :builds %li.hidden = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml deleted file mode 100644 index 665725f6862..00000000000 --- a/app/views/layouts/nav/_project_settings.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- if project_nav_tab? :team - = nav_link(controller: [:members, :teams]) do - = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - %span - Members -- if can_edit - = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - %span - Deploy Keys - = nav_link(controller: :integrations) do - = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - %span - Protected Branches - - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do - %span - CI/CD Pipelines - = nav_link(controller: :pages) do - = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do - %span - Pages diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml deleted file mode 100644 index 060b50ffc69..00000000000 --- a/app/views/notify/build_fail_email.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- content_for :header do - %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (job failed) - -%h3 - Project: - = link_to namespace_project_url(@project.namespace, @project) do - = @project.name - -%p - Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} -%p - Author: #{@build.pipeline.git_author_name} -%p - Branch: #{@build.ref} -%p - Stage: #{@build.stage} -%p - Job: #{@build.name} -%p - Message: #{@build.pipeline.git_commit_message} - -%p - Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb deleted file mode 100644 index 2a94688a6b0..00000000000 --- a/app/views/notify/build_fail_email.text.erb +++ /dev/null @@ -1,11 +0,0 @@ -Job failed for <%= @project.name %> - -Status: <%= @build.status %> -Commit: <%= @build.pipeline.short_sha %> -Author: <%= @build.pipeline.git_author_name %> -Branch: <%= @build.ref %> -Stage: <%= @build.stage %> -Job: <%= @build.name %> -Message: <%= @build.pipeline.git_commit_message %> - -Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml deleted file mode 100644 index ca0eaa96a9d..00000000000 --- a/app/views/notify/build_success_email.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- content_for :header do - %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (job successful) - -%h3 - Project: - = link_to namespace_project_url(@project.namespace, @project) do - = @project.name - -%p - Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} -%p - Author: #{@build.pipeline.git_author_name} -%p - Branch: #{@build.ref} -%p - Stage: #{@build.stage} -%p - Job: #{@build.name} -%p - Message: #{@build.pipeline.git_commit_message} - -%p - Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb deleted file mode 100644 index 445cd46e64f..00000000000 --- a/app/views/notify/build_success_email.text.erb +++ /dev/null @@ -1,11 +0,0 @@ -Job successful for <%= @project.name %> - -Status: <%= @build.status %> -Commit: <%= @build.pipeline.short_sha %> -Author: <%= @build.pipeline.git_author_name %> -Branch: <%= @build.ref %> -Stage: <%= @build.stage %> -Job: <%= @build.name %> -Message: <%= @build.pipeline.git_commit_message %> - -Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index d9ebbaa2704..85a1aea3a61 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -1,179 +1,109 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } +%tr.alert + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + Your pipeline has failed. +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } + = @pipeline.ref + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - %tr.alert - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has failed. - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } - = namespace_name - \/ - %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } - = @project.name - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } - = @pipeline.ref - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = @pipeline.short_sha - - if @merge_request - in - %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } - = @merge_request.to_reference - .commit{ style: "color:#5c5c5c;font-weight:300;" } - = @pipeline.git_commit_message.truncate(50) - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - - commit = @pipeline.commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - - if commit.author - %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } - = commit.author.name - - else - %span - = commit.author_name - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - - - failed = @pipeline.statuses.latest.failed - %tr.pre-section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" - had - = failed.size - failed - #{'build'.pluralize(failed.size)}. - %tr.warning - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } - Logs may contain sensitive data. Please consider before forwarding this email. - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } - %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } - %tbody - - failed.each do |build| - %tr.build-state - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } - %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } - = build.stage - %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build - %tr.build-log - - if build.has_trace? - %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } - %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } - = build.trace_html(last_lines: 10).html_safe - - else - %td{ colspan: "2" } - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = @pipeline.short_sha + - if @merge_request + in + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } + = @merge_request.to_reference + .commit{ style: "color:#5c5c5c;font-weight:300;" } + = @pipeline.git_commit_message.truncate(50) + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + - commit = @pipeline.commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.author + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } + = commit.author.name + - else + %span + = commit.author_name +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + +- failed = @pipeline.statuses.latest.failed +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + had + = failed.size + failed + #{'build'.pluralize(failed.size)}. +%tr.warning + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } + Logs may contain sensitive data. Please consider before forwarding this email. +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } + %tbody + - failed.each do |build| + %tr.build-state + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } + %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } + = build.stage + %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } + = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build + %tr.build-log + - if build.has_trace? + %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } + %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } + = build.trace_html(last_lines: 10).html_safe + - else + %td{ colspan: "2" } diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index ab91c7ef350..520a2fc7d68 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -27,7 +27,3 @@ Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> <% end -%> <% end -%> - -You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. -Manage all notifications: <%= profile_notifications_url %> -Help: <%= help_url %> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 8add2e18206..19d4add06f5 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,154 +1,84 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } +%tr.success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/ %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + Your pipeline has passed. +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + +%tr.section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } + = namespace_name + \/ + %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } + = @project.name + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } + = @pipeline.ref + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = @pipeline.short_sha + - if @merge_request + in + %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } + = @merge_request.to_reference + .commit{ style: "color:#5c5c5c;font-weight:300;" } + = @pipeline.git_commit_message.truncate(50) + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - %tr.success - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has passed. - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - - %tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } - - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) - %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } - = namespace_name - \/ - %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" } - = @project.name - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } - = @pipeline.ref - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = @pipeline.short_sha - - if @merge_request - in - %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" } - = @merge_request.to_reference - .commit{ style: "color:#5c5c5c;font-weight:300;" } - = @pipeline.git_commit_message.truncate(50) - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - - commit = @pipeline.commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - - if commit.author - %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } - = commit.author.name - - else - %span - = commit.author_name - %tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - - %tr.success-message - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } - - build_count = @pipeline.statuses.latest.size - - stage_count = @pipeline.stages_count - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" - successfully completed - #{build_count} #{'build'.pluralize(build_count)} - in - #{stage_count} #{'stage'.pluralize(stage_count)}. - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + - commit = @pipeline.commit + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.author + %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } + = commit.author.name + - else + %span + = commit.author_name +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + +%tr.success-message + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } + - build_count = @pipeline.statuses.latest.size + - stage_count = @pipeline.stages_count + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + successfully completed + #{build_count} #{'build'.pluralize(build_count)} + in + #{stage_count} #{'stage'.pluralize(stage_count)}. diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index 40e5e306426..0970a3a4e09 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -18,7 +18,3 @@ Commit Author: <%= commit.author_name %> <% build_count = @pipeline.statuses.latest.size -%> <% stage_count = @pipeline.stages_count -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. - -You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. -Manage all notifications: <%= profile_notifications_url %> -Help: <%= help_url %> diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml index 1df04ea614e..83ae9129807 100644 --- a/app/views/profiles/_head.html.haml +++ b/app/views/profiles/_head.html.haml @@ -1,3 +1,2 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/cropper.js') = page_specific_javascript_bundle_tag('profile') diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a4f4079d556..8a994f6d600 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -93,7 +93,7 @@ %p Changing your username will change path to all personal projects! .col-lg-9 - = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f| + = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| .form-group = f.label :username, "Path", class: "label-light" .input-group @@ -115,7 +115,7 @@ %h4.prepend-top-0.danger-title Remove account .col-lg-9 - - if @user.can_be_removed? + - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting an account has the following effects: %ul @@ -131,4 +131,7 @@ %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete your account. + - else + %p + You don't have access to delete this user. .append-bottom-default diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 51c4e8e5a73..5c5e5940365 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -34,11 +34,6 @@ .clearfix - = form_for @user, url: profile_notifications_path, method: :put do |f| - %label{ for: 'user_notified_of_own_activity' } - = f.check_box :notified_of_own_activity - %span Receive notifications about your own activity - %hr %h5 Groups (#{@group_notifications.count}) diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml deleted file mode 100644 index 3f6efa33953..00000000000 --- a/app/views/profiles/personal_access_tokens/_form.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- personal_access_token = local_assigns.fetch(:personal_access_token) -- scopes = local_assigns.fetch(:scopes) - -= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| - - = form_errors(personal_access_token) - - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true - - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control" - - .form-group - = f.label :scopes, class: 'label-light' - = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes - - .prepend-top-default - = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index b10f5fc08e2..0645ecad496 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -24,82 +24,11 @@ %hr - %h5.prepend-top-0 - Add a Personal Access Token - %p.profile-settings-content - Pick a name for the application, and we'll give you a unique token. - - = render "form", personal_access_token: @personal_access_token, scopes: @scopes - - %hr - - %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length}) - - - if @active_personal_access_tokens.present? - .table-responsive - %table.table.active-personal-access-tokens - %thead - %tr - %th Name - %th Created - %th Expires - %th Scopes - %th - %tbody - - @active_personal_access_tokens.each do |token| - %tr - %td= token.name - %td= token.created_at.to_date.to_s(:medium) - %td - - if token.expires_at.present? - = token.expires_at.to_date.to_s(:medium) - - else - %span.personal-access-tokens-never-expires-label Never - %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } - - - else - .settings-message.text-center - You don't have any active tokens yet. - - %hr - - %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length}) - - - if @inactive_personal_access_tokens.present? - .table-responsive - %table.table.inactive-personal-access-tokens - %thead - %tr - %th Name - %th Created - %tbody - - @inactive_personal_access_tokens.each do |token| - %tr - %td= token.name - %td= token.created_at.to_date.to_s(:medium) - - - else - .settings-message.text-center - There are no inactive tokens. + = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes + = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens :javascript - var $dateField = $('#personal_access_token_expires_at'); - var date = $dateField.val(); - - new Pikaday({ - field: $dateField.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - minDate: new Date(), - onSelect: function(dateText) { - $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - } - }); - $("#created-personal-access-token").click(function() { this.select(); }); - - $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000); diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index d551754a2e5..c74b3249a13 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -18,7 +18,7 @@ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default - = link_to avatar_icon(@user, 400), target: '_blank' do + = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' %h5.prepend-top-0 Upload new avatar diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 558a1d56151..7ade5f00d47 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -4,7 +4,7 @@ - if inject_u2f_api? - content_for :page_specific_javascripts do - = page_specific_javascript_tag('u2f.js') + = page_specific_javascript_bundle_tag('u2f') .row.prepend-top-default .col-lg-3 @@ -96,4 +96,3 @@ :javascript var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; $(".flash-alert").append(button); - diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml deleted file mode 100644 index 5307e0b48cb..00000000000 --- a/app/views/profiles/update_username.js.haml +++ /dev/null @@ -1,7 +0,0 @@ -- if @user.valid? - :plain - new Flash("Username successfully changed", "notice") -- else - - error = @user.errors.full_messages.first - :plain - new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert") diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 0ea733cb978..aa0cb3e1a50 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -2,10 +2,9 @@ %div{ class: container_class } .nav-block.activity-filter-block - - if current_user - .controls - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do - = icon('rss') + .controls + = link_to namespace_project_path(@project.namespace, @project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do + = icon('rss') = render 'shared/event_filter' diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml new file mode 100644 index 00000000000..db08b77c8e0 --- /dev/null +++ b/app/views/projects/_head.html.haml @@ -0,0 +1,20 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: 'Project home', class: 'shortcuts-project' do + %span + Home + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics', class: 'shortcuts-project-cycle-analytics' do + %span + Cycle Analytics diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 27d25a6b682..61420fd0fb6 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -2,8 +2,8 @@ .form-group .checkbox.builds-feature - = form.label :only_allow_merge_if_build_succeeds do - = form.check_box :only_allow_merge_if_build_succeeds + = form.label :only_allow_merge_if_pipeline_succeeds do + = form.check_box :only_allow_merge_if_pipeline_succeeds %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr @@ -13,3 +13,7 @@ = form.label :only_allow_merge_if_all_discussions_are_resolved do = form.check_box :only_allow_merge_if_all_discussions_are_resolved %strong Only allow merge requests to be merged if all discussions are resolved + .checkbox + = form.label :printing_merge_request_link_enabled do + = form.check_box :printing_merge_request_link_enabled + %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 3c0f01cbf6f..27c8e3c7fca 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,4 +1,5 @@ - page_title "Activity" += render "projects/head" = render 'projects/last_push' diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 8a40281e28c..4ad77b6266d 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -7,13 +7,8 @@ #blob-content-holder.tree-holder .file-holder - .js-file-title.file-title - = blob_icon @blob.mode, @blob.name - %strong - = @path - %small= number_to_human_size @blob.size - .file-actions - = render "projects/blob/actions" + = render "projects/blob/header", blob: @blob + .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml deleted file mode 100644 index 7b9cfbbd067..00000000000 --- a/app/views/projects/blob/_actions.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -.btn-group - = view_on_environment_button(@commit.sha, @path, @environment) if @environment - -.btn-group.tree-btn-group - = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), - class: 'btn btn-sm', target: '_blank' - -# only show normal/blame view links for text files - - if blob_text_viewable?(@blob) - - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) - = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), - class: 'btn btn-sm' - - else - = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), - class: 'btn btn-sm' unless @blob.empty? - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), - class: 'btn btn-sm' - = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' - -- if current_user - .btn-group{ role: "group" } - - if blob_text_viewable?(@blob) - = edit_blob_link - = replace_blob_link - = delete_blob_link diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 19fa4c78501..2b2ee6ed987 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -18,18 +18,11 @@ - else = link_to title, '#' -%ul.blob-commit-info.table-list.hidden-xs +%ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref #blob-content-holder.blob-content-holder %article.file-holder - .js-file-title.file-title - = blob_icon blob.mode, blob.name - %strong - = blob.name - %small - = number_to_human_size(blob_size(blob)) - .file-actions.hidden-xs - = render "actions" - = render blob, blob: blob + = render "projects/blob/header", blob: blob + = render blob.to_partial_path(@project), blob: blob diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml new file mode 100644 index 00000000000..deeeae3d64a --- /dev/null +++ b/app/views/projects/blob/_header.html.haml @@ -0,0 +1,39 @@ +.js-file-title.file-title-flex-parent + .file-header-content + = blob_icon blob.mode, blob.name + + %strong.file-title-name + = blob.name + + = copy_file_path_button(blob.path) + + %small + = number_to_human_size(blob_size(blob)) + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = copy_blob_content_button(blob) if blob_text_viewable?(blob) + = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id)) + = view_on_environment_button(@commit.sha, @path, @environment) if @environment + + .btn-group{ role: "group" }< + -# only show normal/blame view links for text files + - if blob_text_viewable?(blob) + - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) + = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), + class: 'btn btn-sm' + - else + = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), + class: 'btn btn-sm js-blob-blame-link' unless blob.empty? + + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), + class: 'btn btn-sm' + + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' + + - if current_user + .btn-group{ role: "group" }< + = edit_blob_link if blob_text_viewable?(blob) + = replace_blob_link + = delete_blob_link diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index f864702d862..ea3cecb86a9 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -9,7 +9,7 @@ - else .nothing-here-block The SVG could not be displayed as it is too large, you can - #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')} + #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')} instead. - else %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index 58524418a67..7b16d266982 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -3,17 +3,17 @@ .nothing-here-block File too large, you can = succeed '.' do - = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank' + = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer' - else - blob.load_all_data!(@repository) - - if markup?(blob.name) - .file-content.wiki - = render_markup(blob.name, blob.data) + - if blob.empty? + .file-content.code + .nothing-here-block Empty file - else - - if blob.empty? - .file-content.code - .nothing-here-block Empty file + - if markup?(blob.name) + .file-content.wiki + = render_markup(blob.name, blob.data) - else = render 'shared/file_highlight', blob: blob, repository: @repository diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index d1f7f65bf53..d1d448f0d4c 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -9,20 +9,20 @@ - line_old = line_new - @form.offset - line_content = capture do %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} - %tr.line_holder{ id: line_old, class: line_class } + %tr.line_holder.diff-expanded{ id: line_old, class: line_class } - case diff_view - when :inline %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{ href: "##{line_old}", data: { linenumber: line_old } } + %a{ href: "#", data: { linenumber: line_old }, disabled: true } %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{ href: "##{line_new}", data: { linenumber: line_new } } + %a{ href: "#", data: { linenumber: line_new }, disabled: true } = line_content - when :parallel %td.old_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_old), "##{line_old}" + %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true } = line_content %td.new_line.diff-line-num{ data: { linenumber: line_new } } - = link_to raw(line_new), "##{line_new}" + %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true } = line_content - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 8853801016b..3bcddcb37f1 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -9,7 +9,7 @@ - if @conflict .alert.alert-danger Someone edited the file the same time you did. Please check out - = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" + = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs. .file-editor diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index f5ca9607823..added3f669b 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -3,16 +3,19 @@ - page_title "Boards" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" - %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card" + %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal = render "projects/issues/head" -= render 'shared/issuable/filter', type: :boards +.hidden-xs.hidden-sm + = render 'shared/issuable/search_bar', type: :boards #board-app.boards-app{ "v-cloak" => true, data: board_data } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml deleted file mode 100644 index 0af40ddf8fe..00000000000 --- a/app/views/projects/boards/components/_blank_state.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%board-blank-state{ "inline-template" => true, - "v-if" => 'list.id == "blank"' } - .board-blank-state - %p - Add the following default lists to your Issue Board with one click: - %ul.board-blank-state-list - %li{ "v-for" => "label in predefinedLabels" } - %span.label-color{ ":style" => "{ backgroundColor: label.color } " } - {{ label.title }} - %p - Starting out with the default set of lists will get you right on the way to making the most of your board. - %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" } - Add default lists - %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" } - Nevermind, I'll use my own diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 72bce4049de..0bca6a786cb 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -32,4 +32,4 @@ ":root-path" => "rootPath", "ref" => "board-list" } - if can?(current_user, :admin_list, @project) - = render "projects/boards/components/blank_state" + %board-blank-state{ "v-if" => 'list.id == "blank"' } diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index f413a5e94c1..4a4dd84d5d2 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -2,33 +2,13 @@ .board-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") - if can? current_user, :create_issue, @project - %board-new-issue{ "inline-template" => true, - ":list" => "list", + %board-new-issue{ ":list" => "list", "v-if" => 'list.type !== "done" && showIssueForm' } - .card.board-new-issue-form - %form{ "@submit" => "submit($event)" } - .flash-container{ "v-if" => "error" } - .flash-alert - An error occured. Please try again. - %label.label-light{ ":for" => 'list.id + "-title"' } - Title - %input.form-control{ type: "text", - "v-model" => "title", - "ref" => "input", - ":id" => 'list.id + "-title"' } - .clearfix.prepend-top-10 - %button.btn.btn-success.pull-left{ type: "submit", - ":disabled" => 'title === ""', - "ref" => "submit-button" } - Submit issue - %button.btn.btn-default.pull-right{ type: "button", - "@click" => "cancel" } - Cancel %ul.board-list{ "ref" => "list", "v-show" => "!loading", ":data-board" => "list.id", ":class" => '{ "is-smaller": showIssueForm }' } - %board-card{ "v-for" => "(issue, index) in orderedIssues", + %board-card{ "v-for" => "(issue, index) in issues", "ref" => "issue", ":index" => "index", ":list" => "list", @@ -37,7 +17,8 @@ ":root-path" => "rootPath", ":disabled" => "disabled", ":key" => "issue.id" } - %li.board-list-count.text-center{ "v-if" => "showCount" } + %li.board-list-count.text-center{ "v-if" => "showCount", + "data-issue-id" => "-1" } = icon("spinner spin", "v-show" => "list.loadingMore" ) %span{ "v-if" => "list.issues.length === list.issuesSize" } Showing all issues diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml deleted file mode 100644 index 891c2c46251..00000000000 --- a/app/views/projects/boards/components/_card.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }', - ":index" => "index", - ":data-issue-id" => "issue.id", - "@mousedown" => "mouseDown", - "@mousemove" => "mouseMove", - "@mouseup" => "showIssue($event)" } - %issue-card-inner{ ":list" => "list", - ":issue" => "issue", - ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath" } diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 19ffe73a08d..9eb610ba9c0 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -27,11 +27,11 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do Compare - = render 'projects/buttons/download', project: @project, ref: branch.name + = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name] - if can?(current_user, :push_code, @project) = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), - class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", + class: "btn btn-remove remove-row js-ajax-loading-spinner #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" }, remote: true, diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index e63bdb38bd8..d3c3e40d518 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -12,12 +12,16 @@ .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + = hidden_field_tag :ref, params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } }) .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 27e81c2bec3..7eb17e887e7 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,4 +1,4 @@ -.content-block.build-header +.content-block.build-header.top-area .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false Job @@ -16,7 +16,10 @@ - if @build.user = render "user" = time_ago_with_tooltip(@build.created_at) - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + .nav-controls + - if can?(current_user, :create_issue, @project) && @build.failed? + = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 228dad528ab..307010edb58 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Jobs" -- trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true %div{ class: container_class } diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index b560ed21f1d..d90d4a27cd6 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,3 +1,5 @@ +- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) } + - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> %button.btn{ 'data-toggle' => 'dropdown' } @@ -24,7 +26,6 @@ %i.fa.fa-download %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - if pipeline - artifacts = pipeline.builds.latest.with_artifacts - if artifacts.any? diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index 5d9a776da89..a5a9e4d0621 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,3 +1,3 @@ - if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) - = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do + = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do Run in IDE (Koding) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 5ea85f9fd4c..09286a1b3c6 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -46,7 +46,7 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail - - if build.manual? + - if build.action? %span.label.label-info manual - if pipeline_link diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml deleted file mode 100644 index 3475fa5f960..00000000000 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ /dev/null @@ -1,92 +0,0 @@ -- status = pipeline.status -- show_commit = local_assigns.fetch(:show_commit, true) -- show_branch = local_assigns.fetch(:show_branch, true) - -%tr.commit - %td.commit-link - = render 'ci/status/badge', status: pipeline.detailed_status(current_user) - - %td - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span.pipeline-id ##{pipeline.id} - %span by - - if pipeline.user - = user_avatar(user: pipeline.user, size: 20) - - else - %span.api.monospace API - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck - - %td.branch-commit - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container.commit-icon - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch - - %td - = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph' - - %td - - if pipeline.duration - %p.duration - = custom_icon("icon_timer") - = duration_in_numbers(pipeline.duration) - - if pipeline.finished_at - %p.finished-at - = icon("calendar") - #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)} - - %td.pipeline-actions.hidden-xs - .controls.pull-right - - artifacts = pipeline.builds.latest.with_artifacts_not_expired - - actions = pipeline.manual_actions - - if artifacts.present? || actions.any? - .btn-group.inline - - if actions.any? - .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' } - = custom_icon('icon_play') - = icon('caret-down', 'aria-hidden' => 'true') - %ul.dropdown-menu.dropdown-menu-align-right - - actions.each do |build| - %li - = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = custom_icon('icon_play') - %span= build.name - - if artifacts.present? - .btn-group - %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' } - = icon("download") - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - artifacts.each do |build| - %li - = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do - = icon("download") - %span Download '#{build.name}' artifacts - - - if can?(current_user, :update_pipeline, pipeline.project) - .cancel-retry-btns.inline - - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do - = icon("repeat") - - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do - = icon("remove") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 421b3db342d..b5f67cae341 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,10 +1,10 @@ - case type.to_s - when 'revert' - label = 'Revert' - - target_label = 'Revert in branch' + - branch_label = 'Revert in branch' - when 'cherry-pick' - label = 'Cherry-pick' - - target_label = 'Pick into branch' + - branch_label = 'Pick into branch' .modal{ id: "modal-#{type}-commit" } .modal-dialog @@ -15,10 +15,10 @@ .modal-body = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch - = label_tag 'target_branch', target_label, class: 'control-label' + = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 - = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) .js-create-merge-request-container @@ -37,4 +37,4 @@ = commit_in_fork_help :javascript - new NewCommitForm($('.js-#{type}-form')) + new NewCommitForm($('.js-#{type}-form'), 'start_branch') diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 4d0b7a5ca85..a0a292d0508 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -34,8 +34,9 @@ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) %li.clearfix = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) - %li.clearfix - = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) + - if can_collaborate_with_project? + %li.clearfix + = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header Download @@ -62,15 +63,15 @@ - if @commit.status .well-segment.pipeline-info - %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do + .status-icon-container{ class: "ci-status-icon-#{@commit.status}" } + = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do = ci_icon_for_status(@commit.status) Pipeline - = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace" - for - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" - %span.ci-status-label - = ci_label_for_status(@commit.status) + = link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace" + = ci_label_for_status(@commit.status) + - if @commit.latest_pipeline.stages.any? + .mr-widget-pipeline-graph + = render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph' in = time_interval_in_words @commit.pipelines.total_duration diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 33917513f37..da5a676274f 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,27 +2,7 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, } } -.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), -} } - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 002e3d345dc..6ab9a80e083 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,33 +9,34 @@ - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do - %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" } + %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" } - .table-list-cell.avatar-cell.hidden-xs + .avatar-cell.hidden-xs = author_avatar(commit, size: 36) - .table-list-cell.commit-content - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status(ref) - .visible-xs-inline - = render_commit_status(commit, ref: ref) - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .commit-detail + .commit-content + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status(ref) + .visible-xs-inline + = render_commit_status(commit, ref: ref) + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - - if commit.description? - %pre.commit-row-description.js-toggle-content - = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) - .commiter - = commit_author_link(commit, avatar: false, size: 24) - committed - #{time_ago_with_tooltip(commit.committed_date)} + - if commit.description? + %pre.commit-row-description.js-toggle-content + = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) + .commiter + = commit_author_link(commit, avatar: false, size: 24) + committed + #{time_ago_with_tooltip(commit.committed_date)} - .table-list-cell.commit-actions.hidden-xs - - if commit.status(ref) - = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" - = link_to_browse_code(project, commit) + .commit-actions.flex-row.hidden-xs + - if commit.status(ref) + = render_commit_status(commit, ref: ref) + = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 64d93e4141c..6f5835cb9be 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list.table-list= render commits, project: @project, ref: @ref + %ul.content-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 904cdb5767f..88c7d7bc44b 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,7 +4,7 @@ - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')} %li.commits-row - %ul.content-list.commit-list.table-list.table-wide + %ul.content-list.commit-list = render commits, project: project, ref: ref - if hidden > 0 diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 80763ce67ca..dd6797f10c0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -11,14 +11,6 @@ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network - - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare - = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches @@ -26,3 +18,19 @@ = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags + + = nav_link(path: 'graphs#show') do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do + Contributors + + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Graph + + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare + + = nav_link(path: 'graphs#charts') do + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do + Charts diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 30bb7412073..2f0b6e39800 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name}:#{@ref} commits" - xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.xmlschema if @commits.any? diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 08cb8a04413..38dbf2ac10b 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -2,8 +2,7 @@ - page_title "Commits", @ref = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = content_for :sub_nav do = render "head" @@ -27,10 +26,9 @@ .control = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - - if current_user && current_user.private_token - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do - = icon("rss") + .control + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits Feed", class: 'btn' do + = icon("rss") %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml index c8f0b547f80..9007f2c24ba 100644 --- a/app/views/projects/cycle_analytics/_overview.html.haml +++ b/app/views/projects/cycle_analytics/_overview.html.haml @@ -9,7 +9,7 @@ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. To set up CA, you must first define a production environment by setting up your CI and then deploy to production. %p - %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more + %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more .col-md-6.overview-image %span.overview-icon = custom_icon ('icon_cycle_analytics_overview') diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5405ff16bea..dd3fa814716 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,9 +1,10 @@ - @no_container = true - page_title "Cycle Analytics" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('cycle_analytics') -= render "projects/pipelines/head" += render "projects/head" #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index d1e3cb14022..ec8fc4c9ee8 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -18,7 +18,7 @@ %span.key-created-at created #{time_ago_with_tooltip(deploy_key.created_at)} .visible-xs-block.visible-sm-block - - if @available_keys.include?(deploy_key) + - if @deploy_keys.key_available?(deploy_key) = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do Enable - else diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index c91bb9c255a..1421da72418 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| - = form_errors(@key) += form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| + = form_errors(@deploy_keys.new_key) .form-group = f.label :title, class: "label-light" = f.text_field :title, class: 'form-control', autofocus: true, required: true diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml new file mode 100644 index 00000000000..4cfbd9add00 --- /dev/null +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -0,0 +1,34 @@ +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + Deploy Keys + %p + Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + .col-lg-9 + %h5.prepend-top-0 + Create a new deploy key for this project + = render @deploy_keys.form_partial_path + .col-lg-9.col-lg-offset-3 + %hr + .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys + %h5.prepend-top-0 + Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size}) + - if @deploy_keys.any_keys_enabled? + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key + - else + .settings-message.text-center + No deploy keys found. Create one with the form above. + %h5.prepend-top-default + Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size}) + - if @deploy_keys.any_available_project_keys_enabled? + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key + - else + .settings-message.text-center + No deploy keys from your projects could be found. Create one with the form above or add existing one below. + - if @deploy_keys.any_available_public_keys_enabled? + %h5.prepend-top-default + Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size}) + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml deleted file mode 100644 index 04fbb37d93f..00000000000 --- a/app/views/projects/deploy_keys/index.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- page_title "Deploy Keys" - -.row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = page_title - %p - Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. - .col-lg-9 - %h5.prepend-top-0 - Create a new deploy key for this project - = render "form" - .col-lg-9.col-lg-offset-3 - %hr - .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys - %h5.prepend-top-0 - Enabled deploy keys for this project (#{@enabled_keys.size}) - - if @enabled_keys.any? - %ul.well-list - = render @enabled_keys - - else - .settings-message.text-center - No deploy keys found. Create one with the form above or add existing one below. - %h5.prepend-top-default - Deploy keys from projects you have access to (#{@available_project_keys.size}) - - if @available_project_keys.any? - %ul.well-list - = render @available_project_keys - - else - .settings-message.text-center - No deploy keys from your projects could be found. Create one with the form above or add existing one below. - - if @available_public_keys.any? - %h5.prepend-top-default - Public deploy keys available to any project (#{@available_public_keys.size}) - %ul.well-list - = render @available_public_keys diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index a680b1ca017..506246f2ee6 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,9 +1,9 @@ - if can?(current_user, :create_deployment, deployment) - actions = deployment.manual_actions - if actions.present? - .inline + .btn-group .dropdown - %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -12,4 +12,3 @@ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = custom_icon('icon_play') %span= action.name.humanize - diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index c468202569f..260c9023daf 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,6 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - .pull-right + .pull-right.btn-group = render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 1dbfe830d52..7d6b3701f95 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -2,18 +2,21 @@ - if defined?(blob) && blob && diff_file.submodule? %span = icon('archive fw') - %span + + %strong.file-title-name = submodule_link(blob, diff_commit.id, project.repository) + + = copy_file_path_button(blob.path) - else = conditional_link_to url.present?, url do = blob_icon diff_file.b_mode, diff_file.file_path - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } = old_path → - %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } = new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } @@ -21,7 +24,7 @@ - if diff_file.deleted_file deleted - = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + = copy_file_path_button(diff_file.new_path) - if diff_file.mode_changed? %small diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index cd18ba2ed00..c09c7b87e24 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,22 +1,27 @@ - email = local_assigns.fetch(:email, false) - plain = local_assigns.fetch(:plain, false) +- discussions = local_assigns.fetch(:discussions, nil) - type = line.type - line_code = diff_file.line_code(line) -%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } +- if discussions && !line.meta? + - discussion = discussions[line_code] +%tr.line_holder{ class: type, id: (line_code unless plain) } - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } + - if discussion && discussion.resolvable? && !plain + %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos - if plain @@ -29,9 +34,6 @@ - else = diff_line_content(line.text) -- discussions = local_assigns.fetch(:discussions, nil) -- if discussions && !line.meta? - - discussion = discussions[line_code] - - if discussion - - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) - = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded +- if discussion + - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) + = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 997bf0fc560..b7346f27ddb 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -4,19 +4,24 @@ - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] + - last_line = right.new_pos if right + - unless @diff_notes_disabled + - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) %tr.line_holder.parallel - if left - case left.type - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.line_content.match= left.text - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } + - if discussion_left && discussion_left.resolvable? + %diff-note-avatars{ "discussion-id" => discussion_left.id } %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell @@ -26,23 +31,23 @@ - case right.type - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.new_line.diff-line-num %td.line_content.match= right.text - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } + - if discussion_right && discussion_right.resolvable? + %diff-note-avatars{ "discussion-id" => discussion_right.id } %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel - - unless @diff_notes_disabled - - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - - if discussion_left || discussion_right - = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right + - if discussion_left || discussion_right + = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any? - last_line = diff_file.diff_lines.last - if last_line.new_pos < total_lines diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b9300efd04f..2802a4eca7b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,3 +1,4 @@ += render "projects/settings/head" .project-edit-container .row.prepend-top-default .col-lg-3.profile-settings-sidebar @@ -120,7 +121,7 @@ .form-group - if @project.avatar? .avatar-container.s160 - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 58c085cdb9d..85e442e115c 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -5,6 +5,7 @@ = render 'shared/no_ssh' = render 'shared/no_password' += render "projects/head" = render "home_panel" .row-content-block.second-block.center diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index 4c8fe1c271b..bf0f1819073 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,3 +1,3 @@ - if environment.external_url && can?(current_user, :read_environment, environment) - = link_to environment.external_url, target: '_blank', class: 'btn external-url' do + = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = icon('external-link') diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml new file mode 100644 index 00000000000..acbac1869fd --- /dev/null +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -0,0 +1,6 @@ +- environment = local_assigns.fetch(:environment) + +- return unless environment.has_metrics? && can?(current_user, :read_environment, environment) + += link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do + = icon('area-chart') diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index d9cb7bc0331..4b101447bc0 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -3,6 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments_folder") #environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 1f27d41ddd9..80d2b6f5d95 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,6 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments") #environments-list-view{ data: { environments_data: environments_list_data, @@ -13,7 +14,4 @@ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "help-page-path" => help_page_path("ci/environments"), - "css-class" => container_class, - "commit-icon-svg" => custom_icon("icon_commit"), - "terminal-icon-svg" => custom_icon("icon_terminal"), - "play-icon-svg" => custom_icon("icon_play") } } + "css-class" => container_class } } diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml new file mode 100644 index 00000000000..b8c1782f050 --- /dev/null +++ b/app/views/projects/environments/metrics.html.haml @@ -0,0 +1,24 @@ +- @no_container = true +- page_title "Metrics for environment", @environment.name +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') + = page_specific_javascript_bundle_tag('monitoring') += render "projects/pipelines/head" + +%div{ class: container_class } + .top-area + .row + .col-sm-6 + %h3.page-title + Environment: + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7036325fff8..f463a429f65 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name .col-md-3 .nav-controls + = render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) @@ -15,7 +16,7 @@ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop? = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post - .deployments-container + .environments-container - if @deployments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml deleted file mode 100644 index 67018aaa2ac..00000000000 --- a/app/views/projects/graphs/_head.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - - - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_chart') - = page_specific_javascript_bundle_tag('graphs') - = nav_link(action: :show) do - = link_to 'Contributors', namespace_project_graph_path - = nav_link(action: :commits) do - = link_to 'Commits', commits_namespace_project_graph_path - = nav_link(action: :languages) do - = link_to 'Languages', languages_namespace_project_graph_path - - if @project.feature_available?(:builds, current_user) - = nav_link(action: :ci) do - = link_to ci_namespace_project_graph_path do - Continuous Integration diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml new file mode 100644 index 00000000000..464ac34d961 --- /dev/null +++ b/app/views/projects/graphs/charts.html.haml @@ -0,0 +1,127 @@ +- @no_container = true +- page_title "Charts" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') + = page_specific_javascript_bundle_tag('graphs') += render "projects/commits/head" + +.repo-charts{ class: container_class } + %h4.sub-header + Programming languages used in this repository + + .row + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') + + = language[:label] + .pull-right + = language[:value] + \% + .col-md-8 + %canvas#languages-chart{ height: 400 } + +.repo-charts{ class: container_class } + .sub-header-block.border-top + + .row.tree-ref-header + .col-md-6 + %h4 + Commit statistics for + %strong= @ref + #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} + + .col-md-6 + .tree-ref-container + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + + .row + .col-md-6 + %ul.commit-stats + %li + Total: + %strong #{@commits_graph.commits.size} commits + %li + Average per day: + %strong #{@commits_graph.commit_per_day} commits + %li + Authors: + %strong= @commits_graph.authors + .col-md-6 + %div + %p.slead + Commits per day of month + %canvas#month-chart + .row + .col-md-6 + .col-md-6 + %div + %p.slead + Commits per weekday + %canvas#weekday-chart + .row + .col-md-6 + .col-md-6 + %div + %p.slead + Commits per day hour (UTC) + %canvas#hour-chart + +:javascript + var responsiveChart = function (selector, data) { + var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }; + // get selector by context + var ctx = selector.get(0).getContext("2d"); + // pointing parent container to make chart.js inherit its width + var container = $(selector).parent(); + var generateChart = function() { + selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; + + var chartData = function (keys, values) { + var data = { + labels : keys, + datasets : [{ + fillColor : "rgba(220,220,220,0.5)", + strokeColor : "rgba(220,220,220,1)", + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data : values + }] + }; + return data; + }; + + var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}); + responsiveChart($('#hour-chart'), hourData); + + var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}); + responsiveChart($('#weekday-chart'), dayData); + + var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); + responsiveChart($('#month-chart'), monthData); + + var data = #{@languages.to_json}; + var ctx = $("#languages-chart").get(0).getContext("2d"); + var options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false + } + var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml deleted file mode 100644 index 6be4273b6ab..00000000000 --- a/app/views/projects/graphs/ci.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- @no_container = true -- page_title "Continuous Integration", "Graphs" -= render 'head' - -%div{ class: container_class } - .sub-header-block - .oneline - A collection of graphs for Continuous Integration - - #charts.ci-charts - .row - .col-md-6 - = render 'projects/graphs/ci/overall' - .col-md-6 - = render 'projects/graphs/ci/build_times' - - %hr - = render 'projects/graphs/ci/builds' diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml deleted file mode 100644 index c8a82f7bca3..00000000000 --- a/app/views/projects/graphs/commits.html.haml +++ /dev/null @@ -1,95 +0,0 @@ -- @no_container = true -- page_title "Commits", "Graphs" -= render 'head' - -%div{ class: container_class } - .sub-header-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs - - %p.lead - Commit statistics for - %strong= @ref - #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} - - .row - .col-md-6 - %ul - %li - %p.lead - %strong= @commits_graph.commits.size - commits during - %strong= @commits_graph.duration - days - %li - %p.lead - Average - %strong= @commits_graph.commit_per_day - commits per day - %li - %p.lead - Contributed by - %strong= @commits_graph.authors - authors - .col-md-6 - %div - %p.slead - Commits per day of month - %canvas#month-chart - .row - .col-md-6 - %div - %p.slead - Commits per day hour (UTC) - %canvas#hour-chart - .col-md-6 - %div - %p.slead - Commits per weekday - %canvas#weekday-chart - -:javascript - var responsiveChart = function (selector, data) { - var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }; - // get selector by context - var ctx = selector.get(0).getContext("2d"); - // pointing parent container to make chart.js inherit its width - var container = $(selector).parent(); - var generateChart = function() { - selector.attr('width', $(container).width()); - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8 - } - return new Chart(ctx).Bar(data, options); - }; - // enabling auto-resizing - $(window).resize(generateChart); - return generateChart(); - }; - - var chartData = function (keys, values) { - var data = { - labels : keys, - datasets : [{ - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data : values - }] - }; - return data; - }; - - var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}); - responsiveChart($('#hour-chart'), hourData); - - var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}); - responsiveChart($('#weekday-chart'), dayData); - - var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); - responsiveChart($('#month-chart'), monthData); diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml deleted file mode 100644 index fcfcae0be20..00000000000 --- a/app/views/projects/graphs/languages.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -- @no_container = true -- page_title "Languages", "Graphs" -= render 'head' - -%div{ class: container_class } - .sub-header-block - .oneline - Programming languages used in this repository - - .row - .col-md-8 - %canvas#languages-chart{ height: 400 } - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') - - = language[:label] - .pull-right - = language[:value] - \% - -:javascript - var data = #{@languages.to_json}; - var ctx = $("#languages-chart").get(0).getContext("2d"); - var options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false - } - var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 5ebb939a109..680f8ae6c8f 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,6 +1,9 @@ - @no_container = true -- page_title "Contributors", "Graphs" -= render 'head' +- page_title "Contributors" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') + = page_specific_javascript_bundle_tag('graphs') += render 'projects/commits/head' %div{ class: container_class } .sub-header-block diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 7076f5db015..8b011af78eb 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,8 +1,2 @@ = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue - -:javascript - $('.assign-to-me-link').on('click', function(e){ - $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 4825820c4d9..7a188cb6445 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -7,7 +7,7 @@ = nav_link(controller: :issues) do = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do %span - Issues + List = nav_link(controller: :boards) do = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index a0df0db77c5..4feec09bb5d 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: url_for(params), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) - xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? + xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 8ea1a3a45e1..f3a429d12d9 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -10,26 +10,23 @@ = page_specific_javascript_bundle_tag('filtered_search') = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") + = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') - - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, - @project, - issue: { assignee_id: issues_finder.assignee.try(:id), - milestone_id: issues_finder.milestones.first.try(:id) }), - class: "btn btn-new", - title: "New Issue", - id: "new_issue_link" do - New Issue + = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do + = icon('rss') + = link_to new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New Issue", + id: "new_issue_link" do + New Issue = render 'shared/issuable/search_bar', type: :issues .issues-holder diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 069f3d97943..6ac05bf3afe 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,8 +2,6 @@ - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_vue') .clearfix.detail-page-header .issuable-header @@ -22,37 +20,34 @@ = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") - - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue) - .issuable-actions - .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right.hidden-lg - %ul - - if can?(current_user, :create_issue, @project) - %li - = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' - - if can?(current_user, :update_issue, @issue) - %li - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - %li - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - %li - = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - - if @issue.submittable_as_spam_by?(current_user) - %li - = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' - - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do - New issue - - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + .issuable-actions + .clearfix.issue-btn-group.dropdown + %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } + Options + = icon('caret-down') + .dropdown-menu.dropdown-menu-align-right.hidden-lg + %ul + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' + - if can?(current_user, :update_issue, @issue) + %li + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + %li + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + %li + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if @issue.submittable_as_spam_by?(current_user) - = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' - = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' + + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do + New issue + - if can?(current_user, :update_issue, @issue) + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + - if @issue.submittable_as_spam_by?(current_user) + = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' .issue-details.issuable-details diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml index 09aa401e44a..6da7c317f3a 100644 --- a/app/views/projects/issues/verify.html.haml +++ b/app/views/projects/issues/verify.html.haml @@ -1,4 +1,5 @@ - form = [@project.namespace.becomes(Namespace), @project, @issue] = render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do - = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions]) + = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of]) + = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve]) diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index a80f9aa4c4a..04bd4e8b683 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -2,16 +2,15 @@ This service will be installed on the Mattermost instance at %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host %hr -= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f| += form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project), html: { class: 'js-requires-input'} ) do |f| %h4 Team %p = @teams.one? ? 'The team' : 'Select the team' where the slash commands will be used in - - selected_id = @teams.one? ? @teams.keys.first : 0 - - options = mattermost_teams_options(@teams) - - options = options_for_select(options, selected_id) - = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) - = f.hidden_field(:team_id, value: selected_id) if @teams.one? + - selected_id = @teams.one? ? @teams.first['id'] : nil + - options = options_for_select(mattermost_teams_options(@teams), selected_id) + = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true }) + = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? .help-block - if @teams.one? This is the only available team. @@ -25,7 +24,7 @@ %hr %h4 Command trigger word %p Choose the word that will trigger commands - = f.text_field(:trigger, value: @project.path, class: 'form-control') + = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true) .help-block %p Trigger word must be unique, and can't begin with a slash or contain any spaces. diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml index 96b1d2aee61..15829a3f143 100644 --- a/app/views/projects/mattermosts/new.html.haml +++ b/app/views/projects/mattermosts/new.html.haml @@ -1,3 +1,5 @@ +- @body_class = 'card-content' + .service-installation .inline.pull-right = custom_icon('mattermost_logo', size: 48) diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 88525f4036a..9607a7b5d06 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,8 +1,2 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request - -:javascript - $('.assign-to-me-link').on('click', function(e){ - $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 466ec1475d8..ad14b4e583e 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -21,7 +21,7 @@ selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch - = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } + = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch = dropdown_title("Select source branch") = dropdown_filter("Search branches") @@ -30,7 +30,7 @@ branches: @merge_request.source_branches, selected: f.object.source_branch .panel-footer - = icon('spinner spin', class: 'js-source-loading') + .text-center= icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit .col-md-6 @@ -60,7 +60,7 @@ branches: @merge_request.target_branches, selected: f.object.target_branch .panel-footer - = icon('spinner spin', class: "js-target-loading") + .text-center= icon('spinner spin', class: "js-target-loading") %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index bd72310c16b..e7fcac4c477 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -52,11 +52,6 @@ = spinner :javascript - $('.assign-to-me-link').on('click', function(e){ - $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); -:javascript var merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 521b0694ca9..6682a85ffa6 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,6 +3,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('diff_notes') .merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) } @@ -15,7 +16,7 @@ .pull-right - if @merge_request.source_branch_exists? - if koding_enabled? && @repository.koding_yml - = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do + = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do Run in IDE (Koding) = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch @@ -28,9 +29,9 @@ %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal - %span Request to merge + %span <b>Request to merge</b> %span.label-branch= source_branch_with_namespace(@merge_request) - %span into + %span <b>into</b> %span.label-branch = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? @@ -81,6 +82,7 @@ = render "shared/icons/icon_status_success.svg" %span.line-resolve-text {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved + = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request = render "discussions/jump_to_next" .tab-content#diff-notes-app diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml index eab5be488b5..eab5be488b5 100644 --- a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml +++ b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 1ecd9924d88..51d59280be8 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,6 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_vue') + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 83e6c026ba7..8a96c8dacf6 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -2,7 +2,6 @@ - @bulk_edit = can?(current_user, :admin_merge_request, @project) - page_title "Merge Requests" -= render "projects/issues/head" = render 'projects/last_push' - content_for :page_specific_javascripts do diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 84b6c9ebc5c..f0a23bec5e7 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -2,9 +2,9 @@ - when :success :plain merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); -- when :merge_when_build_succeeds +- when :merge_when_pipeline_succeeds :plain - $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}"); - when :sha_mismatch :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}"); diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 93ed4b68e0e..cde0ce08e14 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -49,7 +49,7 @@ %strong Tip: = succeed '.' do You can also checkout merge requests locally by - = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank' + = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' :javascript $(function(){ diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c676953f729..1298376ac25 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,8 +1,8 @@ - if @pipeline .mr-widget-heading - - %w[success success_with_warnings skipped canceled failed running pending].each do |status| + - %w[success success_with_warnings skipped manual canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } - %div{ class: "ci-status-icon-#{status}" } + %div{ class: "ci-status-icon ci-status-icon-#{status}" } = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do = ci_icon_for_status(status) %span diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 7794d6d7df2..adc3bbc37f3 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -7,28 +7,46 @@ by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} - if !@merge_request.source_branch_exists? || params[:deleted_source_branch] - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - The source branch has been removed. + .remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + %li + %span + The source branch has been removed. = render 'projects/merge_requests/widget/merged_buttons' - elsif @merge_request.can_remove_source_branch?(current_user) - .remove_source_branch_widget - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - You can remove the source branch now. + .remove_source_branch_widget.remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + %li + %span + You can remove the source branch now. = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true - .remove_source_branch_widget.failed.hide - %p - Failed to remove source branch '#{@merge_request.source_branch}'. - - .remove_source_branch_in_progress.hide - %p - = icon('spinner spin') - Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. + .remove_source_branch_widget.failed.remove-message-pipes.hide + %ul + %li + %span + Failed to remove source branch '#{@merge_request.source_branch}'. + .remove_source_branch_in_progress.remove-message-pipes.hide + %ul + %li + %span + = icon('spinner spin') + Removing source branch '#{@merge_request.source_branch}'. + %li + %span + Please wait, this page will be automatically reloaded. - else - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - = render 'projects/merge_requests/widget/merged_buttons' + .remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 9eef011b591..caf3bf54eef 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -9,6 +9,6 @@ = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close") - if mr_can_be_cherry_picked = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index c0d6ab669b8..bc426f1dc0c 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -19,14 +19,16 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' - - elsif @merge_request.merge_when_build_succeeds? - = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' + - elsif @merge_request.merge_when_pipeline_succeeds? + = render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?) = render 'projects/merge_requests/widget/open/build_failed' - elsif !@merge_request.mergeable_discussions_state? = render 'projects/merge_requests/widget/open/unresolved_discussions' + - elsif @pipeline&.blocked? + = render 'projects/merge_requests/widget/open/manual' - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index b730ced4214..c94c7944c0b 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,8 +1,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('merge_request_widget') -- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil - = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token = hidden_field_tag :sha, @merge_request.diff_head_sha @@ -11,24 +9,24 @@ .accept-action - if @pipeline && @pipeline.active? %span.btn-group - = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do + = button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do Merge When Pipeline Succeeds - - unless @project.only_allow_merge_if_build_succeeds? - = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do + - unless @project.only_allow_merge_if_pipeline_succeeds? + = button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') %span.sr-only Select Merge Moment %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } %li - = link_to "#", class: "merge_when_build_succeeds" do + = link_to "#", class: "merge_when_pipeline_succeeds" do = icon('check fw') Merge When Pipeline Succeeds %li - = link_to "#", class: "accept_merge_request" do + = link_to "#", class: "accept-merge-request" do = icon('warning fw') Merge Immediately - else - = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do + = f.button class: "btn btn-grouped js-merge-button accept-merge-request" do Accept Merge Request - if @merge_request.force_remove_source_branch? .accept-control @@ -49,4 +47,4 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true - = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" + = hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off" diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index c98b2c42597..621ee313026 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -3,20 +3,24 @@ - can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user) %h4.has-conflicts - = icon("exclamation-triangle") - This merge request contains merge conflicts + %p + = icon("exclamation-triangle") + This merge request contains merge conflicts -%p - To merge this request, resolve these conflicts - - if can_resolve && !can_resolve_in_ui - locally - or - - unless can_merge - ask someone with write access to this repository to - merge it locally. +.remove-message-pipes + %ul + %li + %span + To merge this request, resolve these conflicts + - if can_resolve && !can_resolve_in_ui + locally + or + - unless can_merge + ask someone with write access to this repository to + merge it locally. - if (can_resolve && can_resolve_in_ui) || can_merge - .btn-group + .merged-buttons.clearfix - if can_resolve && can_resolve_in_ui = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn" - if can_merge diff --git a/app/views/projects/merge_requests/widget/open/_manual.html.haml b/app/views/projects/merge_requests/widget/open/_manual.html.haml new file mode 100644 index 00000000000..9078b7e21dd --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_manual.html.haml @@ -0,0 +1,4 @@ +%h4 + Pipeline blocked +%p + The pipeline for this merge request requires a manual action to proceed. diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml deleted file mode 100644 index cf7abf3756c..00000000000 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('merge_request_widget') - -%h4 - Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} - to be merged automatically when the pipeline succeeds. -%div - %p - = succeed '.' do - The changes will be merged into - %span.label-branch= @merge_request.target_branch - - if @merge_request.remove_source_branch? - The source branch will be removed. - - else - The source branch will not be removed. - - - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - - if remove_source_branch_button || user_can_cancel_automatic_merge - .clearfix.prepend-top-10 - - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do - = icon('times') - Remove Source Branch When Merged - - - if user_can_cancel_automatic_merge - = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do - Cancel Automatic Merge diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml new file mode 100644 index 00000000000..5f347acce4d --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml @@ -0,0 +1,33 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('merge_request_widget') + +%h4 + Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} + to be merged automatically when the pipeline succeeds. +.remove-message-pipes + %ul + %li + %span + = succeed '.' do + The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"} + - if @merge_request.remove_source_branch? + %li + %span + The source branch will be removed. + - else + %li + %span + The source branch will not be removed. + + - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user + - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) + - if remove_source_branch_button || user_can_cancel_automatic_merge + .clearfix.prepend-top-10 + - if remove_source_branch_button + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = icon('times') + Remove Source Branch When Merged + + - if user_can_cancel_automatic_merge + = link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do + Cancel Automatic Merge diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml index e094f97f3b6..ec9346ce89b 100644 --- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -6,5 +6,5 @@ Please resolve these discussions - if @project.issues_enabled? && can?(current_user, :create_issue, @project) or - = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid) + = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: @merge_request.iid) to allow this merge request to be merged. diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 11f41e75e63..55b0b837c6d 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } %h3.page-title - Edit Milestone ##{@milestone.iid} + Edit Milestone #{@milestone.to_reference} %hr diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index ad2bfbec915..918f5d161bb 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,14 +1,14 @@ - @no_container = true -- page_title "Milestones" -= render "projects/issues/head" +- page_title 'Milestones' += render 'projects/issues/head' %div{ class: container_class } .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .nav-controls - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do New Milestone .milestones @@ -19,4 +19,4 @@ %li .nothing-here-block No milestones to show - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: 'gitlab' diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 06a31698ee6..d16f49bd33a 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -19,10 +19,9 @@ Open .header-text-content %span.identifier - Milestone ##{@milestone.iid} + %strong + Milestone #{@milestone.to_reference} - if @milestone.due_date || @milestone.start_date - %span.creator - · = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) @@ -47,7 +46,7 @@ = preserve do = markdown_field(@milestone, :description) - - if @milestone.total_items_count(current_user).zero? + - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default %span Assign some issues to this milestone. - elsif @milestone.complete?(current_user) && @milestone.active? diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b88eef65cef..ed6077f6c6b 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,6 +1,5 @@ -- page_title "Network", @ref +- page_title "Graph", @ref - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/raphael.js') = page_specific_javascript_bundle_tag('network') = render "projects/commits/head" = render "head" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 2a98bba05ee..d129da943f8 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,6 @@ - page_title 'New Project' - header_title "Projects", dashboard_projects_path +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility .project-edit-container .project-edit-errors @@ -95,7 +96,7 @@ = f.label :visibility_level, class: 'label-light' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") - = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false + = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a73e8f345e0..5552086bc50 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ - return if note.cross_reference_not_visible_for?(current_user) - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } .timeline-entry-inner .timeline-icon %a{ href: user_path(note.author) } @@ -30,27 +30,30 @@ - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "discussion-id" => "#{note.discussion_id}", + %resolve-btn{ "project-path" => project_path(note.project), + "discussion-id" => note.discussion_id, ":note-id" => note.id, ":resolved" => note.resolved?, ":can-resolve" => can_resolve, - "resolved-by" => "#{note.resolved_by.try(:name)}", + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{truncate(note.note, length: 17)}'", + ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, "ref" => "note_#{note.id}" } - .note-action-button + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + "v-show" => "!loading", + ":ref" => "'button'" } = icon("spin spinner", "v-show" => "loading") - %button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - "v-show" => "!loading", - ":ref" => "'button'" } - = render "shared/icons/icon_status_success.svg" + = render "shared/icons/icon_status_success.svg" - if current_user - if note.emoji_awardable? diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 08c73d94a09..90a150aa74c 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -23,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 9db46f0b1fc..e442e6e9a09 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -5,4 +5,6 @@ .panel-body %p Learn how to upload your static site and have it served by - GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. + GitLab by following the + = succeed '.' do + = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index b6595269b06..259d5bd63d6 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,4 +1,6 @@ - page_title 'Pages' += render "projects/settings/head" + %h3.page_title Pages diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 721a9b6beb5..a5acb7ac4a5 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -4,25 +4,25 @@ .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } %ul{ class: (container_class) } - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do + = nav_link(path: 'pipelines#index', controller: :pipelines) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do + = nav_link(path: 'builds#index', controller: :builds) do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs - if project_nav_tab? :environments - = nav_link(controller: %w(environments)) do + = nav_link(path: 'environments#index', controller: :environments) do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(controller: %w(cycle_analytics)) do - = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_namespace_project_pipelines_path(@project.namespace, @project), title: 'Charts', class: 'shortcuts-pipelines-charts' do %span - Cycle Analytics + Charts diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index a0b14a7274a..3feb99cfcd7 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,3 +1,5 @@ -- @stage.statuses.latest.each do |status| - %li - = render 'ci/status/dropdown_graph_badge', subject: status +- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status) +- HasStatus::ORDERED_STATUSES.each do |ordered_status| + - grouped_statuses.fetch(ordered_status, []).each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml new file mode 100644 index 00000000000..4a5043aac3c --- /dev/null +++ b/app/views/projects/pipelines/charts.html.haml @@ -0,0 +1,21 @@ +- @no_container = true +- page_title "Charts", "Pipelines" +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_d3') + = page_specific_javascript_bundle_tag('graphs') += render 'head' + +%div{ class: container_class } + .sub-header-block + .oneline + A collection of graphs for Continuous Integration + + #charts.ci-charts + .row + .col-md-6 + = render 'projects/pipelines/charts/overall' + .col-md-6 + = render 'projects/pipelines/charts/build_times' + + %hr + = render 'projects/pipelines/charts/builds' diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/pipelines/charts/_build_times.haml index bb0975a9535..bb0975a9535 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/pipelines/charts/_build_times.haml diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/pipelines/charts/_builds.haml index b6f453b9736..b6f453b9736 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/pipelines/charts/_builds.haml diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml index edc4f7b079f..edc4f7b079f 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/pipelines/charts/_overall.haml diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6e0428e2a31..5d59ce06612 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,23 +5,35 @@ %div{ class: container_class } .top-area %ul.nav-links - %li{ class: active_when(@scope.nil?) }> + %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }> = link_to project_pipelines_path(@project) do All %span.badge.js-totalbuilds-count = number_with_delimiter(@pipelines_count) - %li{ class: active_when(@scope == 'running') }> + %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }> + = link_to project_pipelines_path(@project, scope: :pending) do + Pending + %span.badge + = number_with_delimiter(@pending_count) + + %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }> = link_to project_pipelines_path(@project, scope: :running) do Running %span.badge.js-running-count - = number_with_delimiter(@running_or_pending_count) + = number_with_delimiter(@running_count) + + %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }> + = link_to project_pipelines_path(@project, scope: :finished) do + Finished + %span.badge + = number_with_delimiter(@finished_count) - %li{ class: active_when(@scope == 'branches') }> + %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }> = link_to project_pipelines_path(@project, scope: :branches) do Branches - %li{ class: active_when(@scope == 'tags') }> + %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }> = link_to project_pipelines_path(@project, scope: :tags) do Tags @@ -36,28 +48,7 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), - } } - - .vue-pipelines-index + .vue-pipelines-index += page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 55202725b9e..14a270a3039 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,11 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref + = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 04b19a8c5a7..cf0db943865 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -23,6 +23,6 @@ - if can_admin_project %th %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project} = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index e95a3b1b4c3..b8e885b4d9a 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -10,7 +10,7 @@ = f.label :name, class: 'col-md-2 text-right' do Branch: .col-md-10 - = render partial: "dropdown", locals: { f: f } + = render partial: "projects/protected_branches/dropdown", locals: { f: f } .help-block = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') such as diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml new file mode 100644 index 00000000000..2d8c519c025 --- /dev/null +++ b/app/views/projects/protected_branches/_index.html.haml @@ -0,0 +1,21 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('protected_branches') + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Protected Branches + %p Keep stable branches secure and force developers to use merge requests. + %p.prepend-top-20 + By default, protected branches are designed to: + %ul + %li prevent their creation, if not already created, from everybody except Masters + %li prevent pushes from everybody except Masters + %li prevent <strong>anyone</strong> from force pushing to the branch + %li prevent <strong>anyone</strong> from deleting the branch + %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. + .col-lg-9 + - if can? current_user, :admin_project, @project + = render 'projects/protected_branches/create_protected_branch' + + = render "projects/protected_branches/branches_list" diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 0193800dedf..b2a6b8469a3 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -14,7 +14,7 @@ - else (branch was removed from repository) - = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch } + = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch } - if can_admin_project %td diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml deleted file mode 100644 index b3b419bd92d..00000000000 --- a/app/views/projects/protected_branches/index.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -- page_title "Protected branches" -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('protected_branches') - -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 - = page_title - %p Keep stable branches secure and force developers to use merge requests. - %p.prepend-top-20 - By default, protected branches are designed to: - %ul - %li prevent their creation, if not already created, from everybody except Masters - %li prevent pushes from everybody except Masters - %li prevent <strong>anyone</strong> from force pushing to the branch - %li prevent <strong>anyone</strong> from deleting the branch - %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. - .col-lg-9 - - if can? current_user, :admin_project, @project - = render 'create_protected_branch' - - = render "branches_list" diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 3a323d94cc2..2fb88297fb3 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -4,13 +4,13 @@ %ul.list-unstyled.indent-list %li 1. - = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do Enable custom slash commands = icon('external-link') on your Mattermost installation %li 2. - = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do Add a slash command = icon('external-link') in your Mattermost team with these options: diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index a04fd5035a6..2a1b9d4c465 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -4,7 +4,7 @@ %p This service allows users to perform common operations on this project by entering slash commands in Mattermost. - = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do View documentation = icon('external-link') %p.inline diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 0d973a20d4c..078b7be6865 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -5,7 +5,7 @@ %p This service allows users to perform common operations on this project by entering slash commands in Slack. - = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do View documentation = icon('external-link') %p.inline @@ -57,7 +57,7 @@ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.text-block = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') .form-group = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml new file mode 100644 index 00000000000..88bcb541dac --- /dev/null +++ b/app/views/projects/settings/_head.html.haml @@ -0,0 +1,33 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(controller: :projects) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :members) do + = link_to project_settings_members_path(@project), title: 'Members' do + %span + Members + - if can_edit + = nav_link(controller: :integrations) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do + %span + Integrations + = nav_link(controller: :repository) do + = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do + %span + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do + %span + CI/CD Pipelines + = nav_link(controller: :pages) do + = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do + %span + Pages diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 52f5f7b81e2..e2603096014 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,4 +1,5 @@ - page_title "CI/CD Pipelines" += render "projects/settings/head" = render 'projects/runners/index' = render 'projects/variables/index' diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index aa38a889cdd..f69992566b5 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,3 +1,4 @@ - page_title 'Integrations' += render "projects/settings/head" = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index d81ed7bb609..20e1ad68244 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -1,4 +1,5 @@ - page_title "Members" += render "projects/settings/head" = render "projects/project_members/index" - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml new file mode 100644 index 00000000000..4c02302e161 --- /dev/null +++ b/app/views/projects/settings/repository/show.html.haml @@ -0,0 +1,5 @@ +- page_title "Repository" += render "projects/settings/head" + += render @deploy_keys += render "projects/protected_branches/index" diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 11310d5e1e1..5c7f2e315f0 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -1,7 +1,7 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} activity" - xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) xml.updated @events[0].updated_at.xmlschema if @events[0] diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f7419728719..de1229d58aa 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,80 +1,80 @@ - @no_container = true = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") + = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, rss_url_options), title: "#{@project.name} activity") = content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' -= render 'projects/last_push' += render "projects/head" += render "projects/last_push" = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - .project-stats-container{ class: container_class } - %nav.project-stats - %ul.nav - %li - = link_to project_files_path(@project) do - Files (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) + %nav.project-stats{ class: container_class } + %ul.nav + %li + = link_to project_files_path(@project) do + Files (#{storage_counter(@project.statistics.total_repository_size)}) + %li + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) + %li + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) + %li + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if default_project_view != 'readme' && @repository.readme - %li - = link_to 'Readme', readme_path(@project) + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) - - if @repository.changelog - %li - = link_to 'Changelog', changelog_path(@project) + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) - - if @repository.license_blob - %li - = link_to license_short_name(@project), license_path(@project) + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) - - if @repository.contribution_guide - %li - = link_to 'Contribution guide', contribution_guide_path(@project) + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) - - if @repository.gitlab_ci_yml - %li - = link_to 'CI configuration', ci_configuration_path(@project) + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to 'Set up Koding', add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do - Set up auto deploy + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - - if @repository.commit + - if @repository.commit + %div{ class: container_class } .project-last-commit = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 6b3d7d4008b..e35385f4cab 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -4,13 +4,7 @@ .project-snippets %article.file-holder.snippet-file-content - .js-file-title.file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' + = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 8ef069b9e05..dffe908e85a 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -23,7 +23,7 @@ = markdown_field(release, :description) .row-fixed-content.controls - = render 'projects/buttons/download', project: @project, ref: tag.name + = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index e4a78fadbeb..cde23e03d54 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,2 +1,4 @@ -- if @repository.tags.empty? +- if @error.present? + new Flash('#{escape_javascript(@error)}', 'alert'); +- elsif @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index e2f132f7742..7f9a44e565f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" .flex-list{ class: container_class } - .top-area.flex-row + .top-area.adjust .nav-text.row-main-content Tags give the ability to mark specific points in history as being important diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 9864be3562a..a2a26039220 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -2,8 +2,7 @@ - page_title @path.presence || "Files", @ref = content_for :meta_tags do - - if current_user - = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = render "projects/commits/head" = render 'projects/last_push' diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml new file mode 100644 index 00000000000..ea32eac2ae2 --- /dev/null +++ b/app/views/projects/triggers/_content.html.haml @@ -0,0 +1,14 @@ +%h4.prepend-top-0 + Triggers +%p.prepend-top-20 + Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will + impersonate their associated user including their access to projects and their project + permissions. +%p.prepend-top-20 + Triggers with the + %span.label.label-primary legacy + label do not have an associated user and only have access to the current project. +%p.append-bottom-0 + = succeed '.' do + Learn more in the + = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml new file mode 100644 index 00000000000..5f708b3a2ed --- /dev/null +++ b/app/views/projects/triggers/_form.html.haml @@ -0,0 +1,11 @@ += form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f| + = form_errors(@trigger) + + - if @trigger.token + .form-group + %label.label-light Token + %p.form-control-static= @trigger.token + .form-group + = f.label :key, "Description", class: "label-light" + = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" + = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index 33883facf9b..cc74e50a5e3 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,35 +1,31 @@ -.row.prepend-top-default.append-bottom-default +.row.prepend-top-default.append-bottom-default.triggers-container .col-lg-3 - %h4.prepend-top-0 - Triggers - %p.prepend-top-20 - Triggers can force a specific branch or tag to get rebuilt with an API call. - %p.append-bottom-0 - = succeed '.' do - Learn more in the - = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' + = render "projects/triggers/content" .col-lg-9 .panel.panel-default .panel-heading %h4.panel-title Manage your project's triggers .panel-body + = render "projects/triggers/form", btn_text: "Add trigger" + %hr - if @triggers.any? - .table-responsive + .table-responsive.triggers-list %table.table %thead %th %strong Token %th + %strong Description + %th + %strong Owner + %th %strong Last used %th = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger - else %p.settings-message.text-center.append-bottom-default - No triggers have been created yet. Add one using the button below. - - = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f| - = f.submit "Add trigger", class: 'btn btn-success' + No triggers have been created yet. Add one using the form above. .panel-footer diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 112b51712ef..ed68e0ed56d 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -1,12 +1,42 @@ %tr %td - %span.monospace= trigger.token + - if can?(current_user, :admin_trigger, trigger) + %span= trigger.token + = clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard") + - else + %span= trigger.short_token + + .label-container + - if trigger.legacy? + %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy + - if !trigger.can_access_project? + %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid + + %td + - if trigger.description? && trigger.description.length > 15 + %span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15) + - else + = trigger.description + + %td + - if trigger.owner + .trigger-owner.sr-only= trigger.owner.name + = user_avatar(user: trigger.owner, size: 20) %td - - if trigger.last_trigger_request - #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago + - if trigger.last_used + #{time_ago_in_words(trigger.last_used)} ago - else Never - %td.text-right - = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm" + %td.text-right.trigger-actions + - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?" + - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?" + - if trigger.owner != current_user && can?(current_user, :manage_trigger, trigger) + = link_to 'Take ownership', take_ownership_namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: take_ownership_confirmation }, method: :post, class: "btn btn-default btn-sm btn-trigger-take-ownership" + - if can?(current_user, :admin_trigger, trigger) + = link_to edit_namespace_project_trigger_path(@project.namespace, @project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do + %i.fa.fa-pencil + - if can?(current_user, :manage_trigger, trigger) + = link_to namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do + %i.fa.fa-trash diff --git a/app/views/projects/triggers/edit.html.haml b/app/views/projects/triggers/edit.html.haml new file mode 100644 index 00000000000..c35df322b9d --- /dev/null +++ b/app/views/projects/triggers/edit.html.haml @@ -0,0 +1,9 @@ +- page_title "Trigger" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h4.prepend-top-0 + Update trigger + = render "form", btn_text: "Save trigger" diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 763c2fea39b..5211ade1a5f 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -4,6 +4,6 @@ New Page = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do Page History - - if can?(current_user, :create_wiki, @project) + - if can?(current_user, :create_wiki, @project) && @page.latest? = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 22004ecacbc..02133d09cdf 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -11,7 +11,7 @@ .results.prepend-top-10 - if @scope == 'commits' - %ul.content-list.commit-list.table-list.table-wide + %ul.content-list.commit-list = render partial: "search/results/commit", collection: @search_objects - else .search-results diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml new file mode 100644 index 00000000000..7799aff6b5b --- /dev/null +++ b/app/views/shared/_branch_switcher.html.haml @@ -0,0 +1,8 @@ +- dropdown_toggle_text = @target_branch || tree_edit_branch += hidden_field_tag 'target_branch', dropdown_toggle_text + +.dropdown + = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'target_branch', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' } + .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches + = render partial: 'shared/projects/blob/branch_page_default' + = render partial: 'shared/projects/blob/branch_page_create' diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index efb207b9916..c2d9ac87b20 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -17,8 +17,9 @@ %strong= parent.full_path + '/' = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, - pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, - title: 'Please choose a group name with no special characters.' + pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, + title: 'Please choose a group name with no special characters.', + "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 1264e524d86..1d4fd71522d 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -2,6 +2,12 @@ - issue_votes = @issuable_meta_data[issuable.id] - upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes - issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes') +- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count + +- if issuable_mr > 0 + %li + = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') + = issuable_mr - if upvotes > 0 %li diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 1744a597c51..bd994cdad01 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -45,11 +45,11 @@ - if current_user && defined?(@project) .label-subscription.inline - if label.is_a?(ProjectLabel) - %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %span= label_subscription_toggle_button_text(label, @project) = icon('spinner spin', class: 'label-subscribe-button-loading') - else - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } } + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } } %span Unsubscribe = icon('spinner spin', class: 'label-subscribe-button-loading') diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 9b67422da2c..10e6c49ae9f 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,4 +1,4 @@ -<svg width="36" height="36" class="tanuki-logo"> +<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 704893b4d5b..57a0eaa919e 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,19 +1,13 @@ -- if @project - - counts = milestone_counts(@project.milestones) - %ul.nav-links %li{ class: milestone_class_for_state(params[:state], 'opened', true) }> = link_to milestones_filter_path(state: 'opened') do Open - - if @project - %span.badge= counts[:opened] + %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - - if @project - %span.badge= counts[:closed] + %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - - if @project - %span.badge= counts[:all] + %span.badge= counts[:all] diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 0c8ac48bb58..3ac5e15d1c4 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -7,7 +7,7 @@ .form-group.branch = label_tag 'target_branch', 'Target branch', class: 'control-label' .col-sm-10 - = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch" + = render 'shared/branch_switcher' .js-create-merge-request-container .checkbox diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml new file mode 100644 index 00000000000..af4cc90f4a7 --- /dev/null +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -0,0 +1,39 @@ +- type = impersonation ? "Impersonation" : "Personal Access" + +%h5.prepend-top-0 + Add a #{type} Token +%p.profile-settings-content + Pick a name for the application, and we'll give you a unique #{type} Token. + += form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control" + + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes + + .prepend-top-default + = f.submit "Create #{type} Token", class: "btn btn-create" + +:javascript + var $dateField = $('.datepicker'); + var date = $dateField.val(); + + new Pikaday({ + field: $dateField.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + minDate: new Date(), + onSelect: function(dateText) { + $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } + }); diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml new file mode 100644 index 00000000000..67a49815478 --- /dev/null +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -0,0 +1,60 @@ +- type = impersonation ? "Impersonation" : "Personal Access" +%hr + +%h5 Active #{type} Tokens (#{active_tokens.length}) +- if impersonation + %p.profile-settings-content + To see all the user's personal access tokens you must impersonate them first. + +- if active_tokens.present? + .table-responsive + %table.table.active-tokens + %thead + %tr + %th Name + %th Created + %th Expires + %th Scopes + - if impersonation + %th Token + %th + %tbody + - active_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) + %td + - if token.expires? + %span{ class: ('text-warning' if token.expires_soon?) } + In #{distance_of_time_in_words_to_now(token.expires_at)} + - else + %span.token-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" + - if impersonation + %td.token-token-container + = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control" + = clipboard_button(clipboard_text: token.token) + - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } +- else + .settings-message.text-center + This user has no active #{type} Tokens. + +%hr + +%h5 Inactive #{type} Tokens (#{inactive_tokens.length}) +- if inactive_tokens.present? + .table-responsive + %table.table.inactive-tokens + %thead + %tr + %th Name + %th Created + %tbody + - inactive_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) +- else + .settings-message.text-center + This user has no inactive #{type} Tokens. diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 0ce0d759e86..a212c714826 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,6 +1,5 @@ .dropdown.inline.prepend-left-10 %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } } - %span.light - if @sort.present? = sort_options_hash[@sort] - else @@ -10,6 +9,8 @@ %li = link_to page_filter_path(sort: sort_value_priority, label: true) do = sort_title_priority + = link_to page_filter_path(sort: sort_value_label_priority, label: true) do + = sort_title_label_priority = link_to page_filter_path(sort: sort_value_recently_created, label: true) do = sort_title_recently_created = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index e2033654018..7a7e3d46796 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -16,7 +16,6 @@ Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' - - else - = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' - else - %h4.text-center There are no issues to show. + %h4 There are no issues to show. + = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index ba5c2dae09d..00fb77bdb3b 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -5,7 +5,7 @@ .col-xs-12.col-sm-6 .text-content %h4 Labels can be applied to issues and merge requests to categorize them. - %p You can also star label to make it a priority label. + %p You can also star a label to make it a priority label. - if can?(current_user, :admin_label, @project) = link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link' = link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link' diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml new file mode 100644 index 00000000000..37589b634fa --- /dev/null +++ b/app/views/shared/groups/_dropdown.html.haml @@ -0,0 +1,18 @@ +.dropdown.inline + %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to filter_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to filter_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to filter_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to filter_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 60ca23ef680..a95020a9be8 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -1,5 +1,6 @@ - group_member = local_assigns[:group_member] - full_name = true unless local_assigns[:full_name] == false +- group_name = full_name ? group.full_name : group.name - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" if group.description.blank? @@ -28,11 +29,7 @@ .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title - = link_to group, class: 'group-name' do - - if full_name - = group.full_name - - else - = group.name + = link_to group_name, group, class: 'group-name' - if group_member as diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml new file mode 100644 index 00000000000..ad7a7faedf1 --- /dev/null +++ b/app/views/shared/groups/_search_form.html.haml @@ -0,0 +1,2 @@ += form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f| + = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" diff --git a/app/views/shared/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb new file mode 100644 index 00000000000..917753fb343 --- /dev/null +++ b/app/views/shared/icons/_collapse.svg.erb @@ -0,0 +1 @@ +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg> diff --git a/app/views/shared/icons/_icon_customization.svg b/app/views/shared/icons/_icon_customization.svg new file mode 100644 index 00000000000..eb1f8ba129b --- /dev/null +++ b/app/views/shared/icons/_icon_customization.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg> diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg new file mode 100644 index 00000000000..d1c541523ab --- /dev/null +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M250.05 34c1.9.04 3.8.11 5.6.2l-29.79 35.51c-.07.01-.15.03-.23.04C149.26 84.1 98.22 146.5 98.22 222.97c0 41.56 23.07 90.5 59.75 119.1 28.61 22.32 64.29 36.9 101.21 36.9 93.4 0 160.15-68.61 160.15-156 0-34.91-15.99-72.77-41.76-100.76l-1.63-47.39c54.45 39.15 89.95 103.02 90.06 175.17v.01c0 119.29-96.7 216-216 216-119.29 0-216-96.71-216-216S130.71 34 250 34h.05zm64.1 20.29c.66-.04 1.32.03 1.96.25 3.01 1 3.85 3.57 3.93 6.45l3.84 146.88c.76 28.66-17.16 68.44-60.39 68.56-30.97.08-63.68-20.83-63.68-60.13.01-14.73 5.61-31.26 19.25-48.11l90.03-111.18c1.15-1.42 3.08-2.58 5.06-2.72z"/></svg> diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg new file mode 100644 index 00000000000..ae219a3ded2 --- /dev/null +++ b/app/views/shared/icons/_icon_mr_issue.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f17ae9f28eb..847a86e2e68 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,4 +1,4 @@ -- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters @@ -24,7 +24,7 @@ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true + = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } @@ -34,21 +34,7 @@ %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters .pull-right - - if boards_page - #js-boards-search.issue-boards-search - %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - - if can?(current_user, :admin_list, @project) - #js-add-issues-btn.pull-right.prepend-left-10 - .dropdown.pull-right - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading - - else - = render 'shared/sort_dropdown' + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index cb92b2e97a7..17107f55a2d 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -8,7 +8,7 @@ .alert.alert-danger Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. Please check out - = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank" + = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs .form-group @@ -45,41 +45,47 @@ = render 'shared/issuable/form/merge_params', issuable: issuable -- if @merge_request_for_resolving_discussions +- if @merge_request_to_resolve_discussions_of .form-group .col-sm-10.col-sm-offset-2 - - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user) - = icon('exclamation-triangle') - Creating this issue will mark all discussions in - = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) - as resolved. - = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid + = icon('info-circle') + - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user) + = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid + - if @discussion_to_resolve + = hidden_field_tag 'discussion_to_resolve', @discussion_to_resolve.id + Creating this issue will resolve the discussion in + - else + Creating this issue will resolve all discussions in + = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve) - else - = icon('exclamation-triangle') - You can't automatically mark all discussions in - = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) - as resolved. Ask someone with sufficient rights to resolve the them. + The + = @discussion_to_resolve ? 'discussion' : 'discussions' + at + = link_to_discussions_to_resolve(@merge_request_to_resolve_discussions_of, @discussion_to_resolve) + will stay unresolved. Ask someone with permission to resolve + = @discussion_to_resolve ? 'it.' : 'them.' - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - - if issuable.new_record? - = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - - else - = form.submit 'Save changes', class: 'btn btn-save' + .pull-right + - if issuable.new_record? + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' + - else + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' + + %span.append-right-10 + - if issuable.new_record? + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + - else + = form.submit 'Save changes', class: 'btn btn-save' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) - .inline.prepend-left-10 + .inline.prepend-top-10 Please review the %strong= link_to('contribution guidelines', guide_url) for this project. - - if issuable.new_record? - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - - else - .pull-right - - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-danger btn-grouped' - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' = form.hidden_field :lock_version diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 415361f8fbf..f0d50828e2a 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -6,7 +6,7 @@ - if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", - placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do + placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project %ul.dropdown-footer-list - if can? current_user, :admin_milestone, project diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8e04b50bb8a..b58640c3ef0 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,7 +1,8 @@ - type = local_assigns.fetch(:type) +- block_css_class = type != :boards_modal ? 'row-content-block second-block' : '' .issues-filters - .issues-details-filters.row-content-block.second-block.filtered-search-block + .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal } = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do - if params[:search].present? = hidden_field_tag :search, params[:search] @@ -11,18 +12,21 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } - = icon('filter') - %button.clear-search.hidden{ type: 'button' } - = icon('times') + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-action' => 'submit' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link -# Encapsulate static class name `{{icon}}` inside #{} to bypass @@ -33,57 +37,72 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link Upcoming + %li.filter-dropdown-item{ 'data-value' => 'started' } + %button.btn.btn-link + Started %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.js-data-value {{title}} - #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right - = render 'shared/sort_dropdown' + .filter-dropdown-container + - if type == :boards + - if can?(current_user, :admin_list, @project) + .dropdown.prepend-left-10#js-add-list + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + Add list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + #js-add-issues-btn.prepend-left-10 + - elsif type != :boards_modal + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide @@ -112,22 +131,23 @@ = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] - .filter-item.inline + .filter-item.inline.update-issues-btn = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" -:javascript - new UsersSelect(); - new LabelsSelect(); - new MilestoneSelect(); - new IssueStatusSelect(); - new SubscriptionSelect(); +- unless type === :boards_modal + :javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); - $(document).off('page:restore').on('page:restore', function (event) { - if (gl.FilteredSearchManager) { - new gl.FilteredSearchManager(); - } - Issuable.init(); - new gl.IssuableBulkActions({ - prefixId: 'issue_', + $(document).off('page:restore').on('page:restore', function (event) { + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } + Issuable.init(); + new gl.IssuableBulkActions({ + prefixId: 'issue_', + }); }); - }); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 3f7f1a86b9f..25a4aec0a38 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,6 @@ - todo = issuable_todo(issuable) - content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('issuable') %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } @@ -9,16 +10,16 @@ - if current_user %span.issuable-header-text.hide-collapsed.pull-left Todo - %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } %span.js-issuable-todo-text - if todo Mark done - else Add todo - = icon('spin spinner', class: 'hidden js-issuable-todo-loading') + = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true') = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| .block.assignee @@ -26,10 +27,10 @@ - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 24) - else - = icon('user') + = icon('user', 'aria-hidden': 'true') .title.hide-collapsed Assignee - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -37,7 +38,7 @@ = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } - = icon('exclamation-triangle') + = icon('exclamation-triangle', 'aria-hidden': 'true') %span.username = issuable.assignee.to_reference - else @@ -54,7 +55,7 @@ .block.milestone .sidebar-collapsed-icon - = icon('clock-o') + = icon('clock-o', 'aria-hidden': 'true') %span - if issuable.milestone %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } @@ -63,7 +64,7 @@ None .title.hide-collapsed Milestone - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -77,20 +78,20 @@ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - if issuable.has_attribute?(:time_estimate) #issuable-time-tracker.block - %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'docs-url' => help_page_path('workflow/time_tracking.md') } // Fallback while content is loading .title.hide-collapsed Time tracking - = icon('spinner spin') + = icon('spinner spin', 'aria-hidden': 'true') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon - = icon('calendar') + = icon('calendar', 'aria-hidden': 'true') %span.js-due-date-sidebar-value = issuable.due_date.try(:to_s, :medium) || 'None' .title.hide-collapsed Due date - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed @@ -110,7 +111,7 @@ .dropdown %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } %span.dropdown-toggle-text Due date - = icon('chevron-down') + = icon('chevron-down', 'aria-hidden': 'true') .dropdown-menu.dropdown-menu-due-date = dropdown_title('Due date') = dropdown_content do @@ -120,12 +121,12 @@ - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } - = icon('tags') + = icon('tags', class: 'hidden', 'aria-hidden': 'true') %span = selected_labels.size .title.hide-collapsed Labels - = icon('spinner spin', class: 'block-loading') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } @@ -141,7 +142,7 @@ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") - = icon('chevron-down') + = icon('chevron-down', 'aria-hidden': 'true') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default" - if can? current_user, :admin_label, @project and @project @@ -152,7 +153,7 @@ - subscribed = issuable.subscribed?(current_user, @project) .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } } .sidebar-collapsed-icon - = icon('rss') + = icon('rss', 'aria-hidden': 'true') %span.issuable-header-text.hide-collapsed.pull-left Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' @@ -173,7 +174,7 @@ :javascript gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); - new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); + new MilestoneSelect('{"full_path":"#{@project.full_path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); gl.Subscription.bindAll('.subscription'); diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index a47085230b8..9dbfedb84f1 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -13,15 +13,15 @@ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}" .form-group.issue-milestone = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index a93cbd1041f..8af3bd597c5 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -13,6 +13,6 @@ - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } = render partial: 'shared/milestones/issuable', - collection: issuables.sort_by(&:position), + collection: issuables.order_position_asc, as: :issuable, locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index d27fba805a3..78079f633d5 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -6,14 +6,15 @@ .milestone-stats-and-buttons .milestone-stats - %span.milestone-stat.with-drilldown - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed + - if !project || can?(current_user, :read_issue, project) + %span.milestone-stat.with-drilldown + %strong= milestone.issues_visible_to_user(current_user).size + issues: + %span.milestone-stat + %strong= milestone.issues_visible_to_user(current_user).opened.size + open and + %strong= milestone.issues_visible_to_user(current_user).closed.size + closed %span.milestone-stat.with-drilldown %strong= milestone.merge_requests.size merge requests: @@ -32,10 +33,12 @@ .milestone-progress-buttons %span.tab-issues-buttons - - if project && can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do - New Issue - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" + - if project + - if can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do + New Issue + - if can?(current_user, :read_issue, project) + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" %span.tab-merge-requests-buttons.hidden = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index c8f2319d95a..a0e9ec46220 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,12 +1,18 @@ %ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= milestone.issues_visible_to_user(current_user).size - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues_visible_to_user(current_user).size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + - else + %li.active + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants @@ -20,10 +26,14 @@ - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) .tab-content.milestone-content - .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + .tab-pane.active#tab-issues + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + - else + .tab-pane.active#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: milestone.participants .tab-pane#tab-labels diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index c19697802ce..2d25b8aad62 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,8 +1,4 @@ - @sort ||= sort_value_recently_updated -- personal = params[:personal] -- archived = params[:archived] -- shared = params[:shared] -- namespace_id = params[:namespace_id] .dropdown - toggle_text = projects_sort_options_hash[@sort] = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) @@ -11,32 +7,32 @@ Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do + = link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do Owned by anyone %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do + = link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do Owned by me - if @group && @group.shared_projects.present? %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do + = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do All projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do + = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do Hide shared projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do + = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do Hide group projects diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 3a9dd37dc7d..c57282c5742 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -8,7 +8,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - remote = false unless local_assigns[:remote] == true -.projects-list-holder +.js-projects-list-holder - if projects.any? %ul.projects-list.content-list - projects.each_with_index do |project, i| @@ -25,6 +25,3 @@ = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages - else .nothing-here-block No projects found - -:javascript - ProjectsList.init(); diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 4a27965754d..df21857e1ad 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -6,17 +6,16 @@ - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description -- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] -- cache_key.push(project.commit.status) if project.commit.try(:status) +- cache_key = project_list_cache_key(project) %li.project-row{ class: css_class } = cache(cache_key) do .controls - if project.archived %span.label.label-warning archived - - if project.commit.try(:status) + - if project.pipeline_status.has_status? %span - = render_commit_status(project.commit) + = render_project_pipeline_status(project.pipeline_status) - if forks %span = icon('code-fork') diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml new file mode 100644 index 00000000000..b89194bcc67 --- /dev/null +++ b/app/views/shared/projects/_search_form.html.haml @@ -0,0 +1,23 @@ += form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = search_field_tag :name, params[:name], + placeholder: 'Filter by name...', + class: 'project-filter-form-field form-control input-short js-projects-list-filter', + spellcheck: false, + id: 'project-filter-form-field', + tabindex: "2", + autofocus: local_assigns[:autofocus] + + - if local_assigns[:icon] + = icon("search", class: "search-icon") + + - if params[:sort].present? + = hidden_field_tag :sort, params[:sort] + + - if params[:personal].present? + = hidden_field_tag :personal, params[:personal] + + - if params[:archived].present? + = hidden_field_tag :archived, params[:archived] + + - if params[:visibility_level].present? + = hidden_field_tag :visibility_level, params[:visibility_level] diff --git a/app/views/shared/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml new file mode 100644 index 00000000000..c279a0d8846 --- /dev/null +++ b/app/views/shared/projects/blob/_branch_page_create.html.haml @@ -0,0 +1,8 @@ +.dropdown-page-two.dropdown-new-branch + = dropdown_title('Create new branch', back: true) + = dropdown_content do + %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" } + %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" } + Create + %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" } + Cancel diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml new file mode 100644 index 00000000000..9bf78d10878 --- /dev/null +++ b/app/views/shared/projects/blob/_branch_page_default.html.haml @@ -0,0 +1,10 @@ +.dropdown-page-one + = dropdown_title "Select branch" + = dropdown_filter "Search branches" + = dropdown_content + = dropdown_loading + = dropdown_footer do + %ul.dropdown-footer-list + %li + %a.create-new-branch.dropdown-toggle-page{ href: "#" } + Create new branch diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index ad5c0c2d8c8..74f71e6cbd1 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,7 +1,25 @@ -- unless @snippet.content.empty? +.js-file-title.file-title-flex-parent + .file-header-content + = blob_icon @snippet.mode, @snippet.path + + %strong.file-title-name + = @snippet.path + + = copy_file_path_button(@snippet.path) + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = copy_blob_content_button(@snippet) + = open_raw_file_button(raw_path) + + - if defined?(download_path) && download_path + = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + +- if @snippet.content.empty? + .file-content.code + .nothing-here-block Empty file +- else - if markup?(@snippet.file_name) - %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } } - = @snippet.content .file-content.wiki - if gitlab_markdown?(@snippet.file_name) = preserve(markdown_field(@snippet, :content)) @@ -9,6 +27,3 @@ = render_markup(@snippet.file_name, @snippet.content) - else = render 'shared/file_highlight', blob: @snippet -- else - .file-content.code - .nothing-here-block Empty file diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index e7f7db73223..0296597b294 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f| = form_errors(@snippet) .form-group diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 970afbe6b64..da9fb755a36 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,13 +3,7 @@ = render 'shared/snippets/header' %article.file-holder.snippet-file-content - .js-file-title.file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm") - = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" - = render 'shared/snippets/blob' + = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet) -= render 'award_emoji/awards_block', awardable: @snippet, inline: true +.row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index dc2fea450bd..dc9a3b0d0df 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,7 +1,7 @@ - page_title @user.name - page_description @user.bio - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('lib_d3') + = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('users') - header_title @user.name, user_path(@user) - @no_container = true @@ -24,17 +24,16 @@ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - - if current_user - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do - = icon('rss') - - if current_user.admin? - = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('users') + = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do + = icon('rss') + - if current_user && current_user.admin? + = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('users') .profile-header .avatar-holder - = link_to avatar_icon(@user, 400), target: '_blank' do + = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' .user-info @@ -98,6 +97,7 @@ Snippets %div{ class: container_class } + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 0e20df506a3..13207a8bc71 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -10,7 +10,7 @@ class AuthorizedProjectsWorker end def self.bulk_perform_async(args_list) - Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) + Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list) end def perform(user_id) diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb deleted file mode 100644 index 5fdb1f2baa0..00000000000 --- a/app/workers/build_email_worker.rb +++ /dev/null @@ -1,20 +0,0 @@ -class BuildEmailWorker - include Sidekiq::Worker - include BuildQueue - - def perform(build_id, recipients, push_data) - recipients.each do |recipient| - begin - case push_data['build_status'] - when 'success' - Notify.build_success_email(build_id, recipient).deliver_now - when 'failed' - Notify.build_fail_email(build_id, recipient).deliver_now - end - # These are input errors and won't be corrected even if Sidekiq retries - rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e - logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}") - end - end - end -end diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 7e44b241743..c9658b3fe17 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -120,8 +120,8 @@ class IrkerWorker end def compare_url(data, repo_path) - sha1 = Commit::truncate_sha(data['before']) - sha2 = Commit::truncate_sha(data['after']) + sha1 = Commit.truncate_sha(data['before']) + sha2 = Commit.truncate_sha(data['after']) compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare" compare_url += "/#{sha1}...#{sha2}" colorize_url compare_url @@ -129,7 +129,7 @@ class IrkerWorker def send_one_commit(project, hook_attrs, repo_name, branch) commit = commit_from_id project, hook_attrs['id'] - sha = colorize_sha Commit::truncate_sha(hook_attrs['id']) + sha = colorize_sha Commit.truncate_sha(hook_attrs['id']) author = hook_attrs['author']['name'] files = colorize_nb_files(files_count commit) title = commit.title diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 2fff6b0105d..2cd87895c55 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -3,8 +3,8 @@ class PostReceive include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) - if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) } - repo_path.gsub!(path[1].to_s, "") + if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) } + repo_path.gsub!(repository_storage[1]['path'].to_s, "") else log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"") end diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb deleted file mode 100644 index b70df5a1afa..00000000000 --- a/app/workers/stuck_ci_builds_worker.rb +++ /dev/null @@ -1,19 +0,0 @@ -class StuckCiBuildsWorker - include Sidekiq::Worker - include CronjobQueue - - BUILD_STUCK_TIMEOUT = 1.day - - def perform - Rails.logger.info 'Cleaning stuck builds' - - builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago) - builds.find_each(batch_size: 50).each do |build| - Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" - build.drop - end - - # Update builds that failed to drop - builds.update_all(status: 'failed') - end -end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb new file mode 100644 index 00000000000..ae8c980c9e4 --- /dev/null +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -0,0 +1,59 @@ +class StuckCiJobsWorker + include Sidekiq::Worker + include CronjobQueue + + EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'.freeze + + BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour + BUILD_PENDING_OUTDATED_TIMEOUT = 1.day + BUILD_PENDING_STUCK_TIMEOUT = 1.hour + + def perform + return unless try_obtain_lease + + Rails.logger.info "#{self.class}: Cleaning stuck builds" + + drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT + drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT + drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT + + remove_lease + end + + private + + def try_obtain_lease + @uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain + end + + def remove_lease + Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid) + end + + def drop(status, timeout) + search(status, timeout) do |build| + drop_build :outdated, build, status, timeout + end + end + + def drop_stuck(status, timeout) + search(status, timeout) do |build| + return unless build.stuck? + drop_build :stuck, build, status, timeout + end + end + + def search(status, timeout) + builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago) + builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| + yield(build) + end + end + + def drop_build(type, build, status, timeout) + Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})" + Gitlab::OptimisticLocking.retry_lock(build, 3) do |b| + b.drop + end + end +end diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb new file mode 100644 index 00000000000..e43bbe35de9 --- /dev/null +++ b/app/workers/system_hook_push_worker.rb @@ -0,0 +1,8 @@ +class SystemHookPushWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(push_data, hook_id) + SystemHooksService.new.execute_hooks(push_data, hook_id) + end +end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index acc4d858136..89ae17cef37 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -10,8 +10,5 @@ class UpdateMergeRequestsWorker return unless user MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) - - push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, []) - SystemHooksService.new.execute_hooks(push_data, :push_hooks) end end diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb new file mode 100644 index 00000000000..78931f1258f --- /dev/null +++ b/app/workers/upload_checksum_worker.rb @@ -0,0 +1,12 @@ +class UploadChecksumWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(upload_id) + upload = Upload.find(upload_id) + upload.calculate_checksum + upload.save! + rescue ActiveRecord::RecordNotFound + Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping") + end +end |