summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/emoji/100.pngbin793 -> 0 bytes
-rw-r--r--app/assets/images/emoji/1234.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/1F627.pngbin821 -> 0 bytes
-rw-r--r--app/assets/images/emoji/8ball.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/a.pngbin469 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ab.pngbin505 -> 0 bytes
-rw-r--r--app/assets/images/emoji/abc.pngbin646 -> 0 bytes
-rw-r--r--app/assets/images/emoji/abcd.pngbin670 -> 0 bytes
-rw-r--r--app/assets/images/emoji/accept.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aerial_tramway.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane.pngbin1152 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_arriving.pngbin1101 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_departure.pngbin1111 -> 0 bytes
-rw-r--r--app/assets/images/emoji/airplane_small.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alarm_clock.pngbin1044 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alembic.pngbin953 -> 0 bytes
-rw-r--r--app/assets/images/emoji/alien.pngbin839 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ambulance.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/amphora.pngbin1044 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anchor.pngbin779 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel.pngbin2077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone1.pngbin2088 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone2.pngbin2075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone3.pngbin2078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone4.pngbin2076 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angel_tone5.pngbin2078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anger.pngbin594 -> 0 bytes
-rw-r--r--app/assets/images/emoji/anger_right.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/angry.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ant.pngbin1412 -> 0 bytes
-rw-r--r--app/assets/images/emoji/apple.pngbin655 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aquarius.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/aries.pngbin711 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_backward.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_down.pngbin543 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_double_up.pngbin535 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_down.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_down_small.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_forward.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_down.pngbin563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_heading_up.pngbin559 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_left.pngbin471 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_left.pngbin520 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_lower_right.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_right.pngbin468 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_right_hook.pngbin644 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up.pngbin507 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_down.pngbin474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_up_small.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_left.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrow_upper_right.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrows_clockwise.pngbin519 -> 0 bytes
-rw-r--r--app/assets/images/emoji/arrows_counterclockwise.pngbin693 -> 0 bytes
-rw-r--r--app/assets/images/emoji/art.pngbin1455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/articulated_lorry.pngbin1710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/asterisk.pngbin627 -> 0 bytes
-rw-r--r--app/assets/images/emoji/astonished.pngbin862 -> 0 bytes
-rw-r--r--app/assets/images/emoji/athletic_shoe.pngbin1595 -> 0 bytes
-rw-r--r--app/assets/images/emoji/atm.pngbin1397 -> 0 bytes
-rw-r--r--app/assets/images/emoji/atom.pngbin912 -> 0 bytes
-rw-r--r--app/assets/images/emoji/avocado.pngbin1520 -> 0 bytes
-rw-r--r--app/assets/images/emoji/b.pngbin391 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby.pngbin1380 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_bottle.pngbin818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_chick.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_symbol.pngbin665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone1.pngbin1392 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone2.pngbin1392 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone3.pngbin1403 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone4.pngbin1413 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baby_tone5.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/back.pngbin562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bacon.pngbin2148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/badminton.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baggage_claim.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/balloon.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ballot_box.pngbin1355 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ballot_box_with_check.pngbin639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bamboo.pngbin1946 -> 0 bytes
-rw-r--r--app/assets/images/emoji/banana.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bangbang.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bank.pngbin1358 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bar_chart.pngbin408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/barber.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/baseball.pngbin1185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball.pngbin1546 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone1.pngbin1492 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone2.pngbin1493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone3.pngbin1492 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone4.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/basketball_player_tone5.pngbin1474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bat.pngbin1190 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone1.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone2.pngbin1231 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone3.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone4.pngbin1252 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bath_tone5.pngbin1239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bathtub.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/battery.pngbin228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beach.pngbin942 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beach_umbrella.pngbin1486 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bear.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bed.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bee.pngbin1378 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beer.pngbin1338 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beers.pngbin2100 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beetle.pngbin1288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/beginner.pngbin545 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bell.pngbin1496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bellhop.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bento.pngbin1127 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone1.pngbin1860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone2.pngbin1866 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone3.pngbin1851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone4.pngbin1852 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bicyclist_tone5.pngbin1840 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bike.pngbin1505 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bikini.pngbin613 -> 0 bytes
-rw-r--r--app/assets/images/emoji/biohazard.pngbin794 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bird.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/birthday.pngbin2219 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_circle.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_joker.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_large_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_medium_small_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_medium_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_nib.pngbin620 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_small_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/black_square_button.pngbin122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blossom.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blowfish.pngbin1620 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_book.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_car.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blue_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/blush.pngbin812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boar.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bomb.pngbin702 -> 0 bytes
-rw-r--r--app/assets/images/emoji/book.pngbin1716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bookmark.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bookmark_tabs.pngbin1395 -> 0 bytes
-rw-r--r--app/assets/images/emoji/books.pngbin2474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boom.pngbin1110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boot.pngbin662 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bouquet.pngbin1662 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_and_arrow.pngbin1402 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone1.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone2.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone3.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone4.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bow_tone5.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bowling.pngbin1426 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boxing_glove.pngbin1575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone1.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone2.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone3.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone4.pngbin870 -> 0 bytes
-rw-r--r--app/assets/images/emoji/boy_tone5.pngbin873 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bread.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil.pngbin2452 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone1.pngbin2464 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone2.pngbin2457 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone3.pngbin2463 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone4.pngbin2463 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bride_with_veil_tone5.pngbin2462 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bridge_at_night.pngbin637 -> 0 bytes
-rw-r--r--app/assets/images/emoji/briefcase.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/broken_heart.pngbin556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bug.pngbin1599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bulb.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_front.pngbin1450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bullettrain_side.pngbin1538 -> 0 bytes
-rw-r--r--app/assets/images/emoji/burrito.pngbin2938 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bus.pngbin1086 -> 0 bytes
-rw-r--r--app/assets/images/emoji/busstop.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/bust_in_silhouette.pngbin426 -> 0 bytes
-rw-r--r--app/assets/images/emoji/busts_in_silhouette.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/butterfly.pngbin1981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cactus.pngbin628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cake.pngbin2266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calendar.pngbin2077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calendar_spiral.pngbin1491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone1.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone2.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone3.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone4.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/call_me_tone5.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/calling.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camel.pngbin1190 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camera.pngbin1783 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camera_with_flash.pngbin2097 -> 0 bytes
-rw-r--r--app/assets/images/emoji/camping.pngbin1513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cancer.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/candle.pngbin1250 -> 0 bytes
-rw-r--r--app/assets/images/emoji/candy.pngbin1054 -> 0 bytes
-rw-r--r--app/assets/images/emoji/canoe.pngbin1244 -> 0 bytes
-rw-r--r--app/assets/images/emoji/capital_abcd.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/capricorn.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/card_box.pngbin1523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/card_index.pngbin1929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/carousel_horse.pngbin1739 -> 0 bytes
-rw-r--r--app/assets/images/emoji/carrot.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel.pngbin1233 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone1.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone2.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone3.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone4.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cartwheel_tone5.pngbin1214 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cat.pngbin1354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cat2.pngbin1781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cd.pngbin908 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chains.pngbin708 -> 0 bytes
-rw-r--r--app/assets/images/emoji/champagne.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/champagne_glass.pngbin1984 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart.pngbin724 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart_with_downwards_trend.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chart_with_upwards_trend.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/checkered_flag.pngbin787 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cheese.pngbin1697 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cherries.pngbin1211 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cherry_blossom.pngbin1129 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chestnut.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chicken.pngbin1267 -> 0 bytes
-rw-r--r--app/assets/images/emoji/children_crossing.pngbin778 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chipmunk.pngbin1454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/chocolate_bar.pngbin771 -> 0 bytes
-rw-r--r--app/assets/images/emoji/christmas_tree.pngbin1542 -> 0 bytes
-rw-r--r--app/assets/images/emoji/church.pngbin1298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cinema.pngbin585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/circus_tent.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/city_dusk.pngbin431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/city_sunset.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cityscape.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cl.pngbin393 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap.pngbin1456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone1.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone2.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone3.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone4.pngbin1458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clap_tone5.pngbin1444 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clapper.pngbin1535 -> 0 bytes
-rw-r--r--app/assets/images/emoji/classical_building.pngbin1006 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clipboard.pngbin1345 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock.pngbin592 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock10.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1030.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock11.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1130.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock12.pngbin480 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock1230.pngbin579 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock130.pngbin526 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock2.pngbin591 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock230.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock3.pngbin482 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock330.pngbin568 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock4.pngbin592 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock430.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock5.pngbin585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock530.pngbin552 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock6.pngbin466 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock630.pngbin536 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock7.pngbin581 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock730.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock8.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock830.pngbin570 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock9.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clock930.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_book.pngbin1359 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_lock_with_key.pngbin1250 -> 0 bytes
-rw-r--r--app/assets/images/emoji/closed_umbrella.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_lightning.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_rain.pngbin876 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_snow.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cloud_tornado.pngbin1519 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clown.pngbin1818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/clubs.pngbin458 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cocktail.pngbin1027 -> 0 bytes
-rw-r--r--app/assets/images/emoji/coffee.pngbin1679 -> 0 bytes
-rw-r--r--app/assets/images/emoji/coffin.pngbin2195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cold_sweat.pngbin971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/comet.pngbin1819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/compression.pngbin1612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/computer.pngbin369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confetti_ball.pngbin1703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confounded.pngbin844 -> 0 bytes
-rw-r--r--app/assets/images/emoji/confused.pngbin647 -> 0 bytes
-rw-r--r--app/assets/images/emoji/congratulations.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_site.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone1.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone2.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone3.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone4.pngbin1095 -> 0 bytes
-rw-r--r--app/assets/images/emoji/construction_worker_tone5.pngbin1119 -> 0 bytes
-rw-r--r--app/assets/images/emoji/control_knobs.pngbin1104 -> 0 bytes
-rw-r--r--app/assets/images/emoji/convenience_store.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cookie.pngbin1351 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cooking.pngbin764 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cool.pngbin396 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop.pngbin1440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone1.pngbin1421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone2.pngbin1424 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone3.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone4.pngbin1417 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cop_tone5.pngbin1433 -> 0 bytes
-rw-r--r--app/assets/images/emoji/copyright.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/corn.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couch.pngbin1362 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple.pngbin1537 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_mm.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_with_heart.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couple_ww.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/couplekiss.pngbin1380 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cow.pngbin1640 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cow2.pngbin1810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cowboy.pngbin1353 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crab.pngbin1475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crayon.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/credit_card.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crescent_moon.pngbin446 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cricket.pngbin1060 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crocodile.pngbin2408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/croissant.pngbin1313 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cross.pngbin408 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crossed_flags.pngbin1239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crossed_swords.pngbin1591 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crown.pngbin1534 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cruise_ship.pngbin2272 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cry.pngbin1123 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crying_cat_face.pngbin1875 -> 0 bytes
-rw-r--r--app/assets/images/emoji/crystal_ball.pngbin1913 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cucumber.pngbin1357 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cupid.pngbin846 -> 0 bytes
-rw-r--r--app/assets/images/emoji/curly_loop.pngbin545 -> 0 bytes
-rw-r--r--app/assets/images/emoji/currency_exchange.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/curry.pngbin1754 -> 0 bytes
-rw-r--r--app/assets/images/emoji/custard.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/customs.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/cyclone.pngbin797 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dagger.pngbin916 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone1.pngbin1420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone2.pngbin1423 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone3.pngbin1429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone4.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancer_tone5.pngbin1418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dancers.pngbin1872 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dango.pngbin802 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dark_sunglasses.pngbin829 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dart.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dash.pngbin840 -> 0 bytes
-rw-r--r--app/assets/images/emoji/date.pngbin788 -> 0 bytes
-rw-r--r--app/assets/images/emoji/deciduous_tree.pngbin1267 -> 0 bytes
-rw-r--r--app/assets/images/emoji/deer.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/department_store.pngbin673 -> 0 bytes
-rw-r--r--app/assets/images/emoji/desert.pngbin1443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/desktop.pngbin311 -> 0 bytes
-rw-r--r--app/assets/images/emoji/diamond_shape_with_a_dot_inside.pngbin693 -> 0 bytes
-rw-r--r--app/assets/images/emoji/diamonds.pngbin247 -> 0 bytes
-rw-r--r--app/assets/images/emoji/disappointed.pngbin757 -> 0 bytes
-rw-r--r--app/assets/images/emoji/disappointed_relieved.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dividers.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dizzy.pngbin795 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dizzy_face.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/do_not_litter.pngbin1010 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dog.pngbin1674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dog2.pngbin2085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dollar.pngbin405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dolls.pngbin2249 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dolphin.pngbin1697 -> 0 bytes
-rw-r--r--app/assets/images/emoji/door.pngbin1105 -> 0 bytes
-rw-r--r--app/assets/images/emoji/doughnut.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dove.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dragon.pngbin1574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dragon_face.pngbin1769 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dress.pngbin1001 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dromedary_camel.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/drooling_face.pngbin1049 -> 0 bytes
-rw-r--r--app/assets/images/emoji/droplet.pngbin411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/drum.pngbin1870 -> 0 bytes
-rw-r--r--app/assets/images/emoji/duck.pngbin1729 -> 0 bytes
-rw-r--r--app/assets/images/emoji/dvd.pngbin933 -> 0 bytes
-rw-r--r--app/assets/images/emoji/e-mail.pngbin1196 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eagle.pngbin2222 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_of_rice.pngbin1422 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone1.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone2.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone3.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone4.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ear_tone5.pngbin860 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_africa.pngbin978 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_americas.pngbin1031 -> 0 bytes
-rw-r--r--app/assets/images/emoji/earth_asia.pngbin966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/egg.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eggplant.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight.pngbin608 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight_pointed_black_star.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eight_spoked_asterisk.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eject.pngbin548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/electric_plug.pngbin548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/elephant.pngbin1293 -> 0 bytes
-rw-r--r--app/assets/images/emoji/end.pngbin393 -> 0 bytes
-rw-r--r--app/assets/images/emoji/envelope.pngbin916 -> 0 bytes
-rw-r--r--app/assets/images/emoji/envelope_with_arrow.pngbin1062 -> 0 bytes
-rw-r--r--app/assets/images/emoji/euro.pngbin460 -> 0 bytes
-rw-r--r--app/assets/images/emoji/european_castle.pngbin965 -> 0 bytes
-rw-r--r--app/assets/images/emoji/european_post_office.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/evergreen_tree.pngbin719 -> 0 bytes
-rw-r--r--app/assets/images/emoji/exclamation.pngbin354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/expressionless.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eye.pngbin664 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eye_in_speech_bubble.pngbin698 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eyeglasses.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/eyes.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm.pngbin1523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone1.pngbin1563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone2.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone3.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone4.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/face_palm_tone5.pngbin1532 -> 0 bytes
-rw-r--r--app/assets/images/emoji/factory.pngbin936 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fallen_leaf.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family.pngbin1433 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmb.pngbin1206 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmbb.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmg.pngbin1361 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmgb.pngbin1626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mmgg.pngbin1448 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwbb.pngbin1638 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwg.pngbin1554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwgb.pngbin1837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_mwgg.pngbin1738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwb.pngbin1155 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwbb.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwg.pngbin1286 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwgb.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/family_wwgg.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fast_forward.pngbin523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fax.pngbin1188 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fearful.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/feet.pngbin603 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fencer.pngbin1342 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ferris_wheel.pngbin2185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ferry.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/field_hockey.pngbin947 -> 0 bytes
-rw-r--r--app/assets/images/emoji/file_cabinet.pngbin1420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/file_folder.pngbin1445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/film_frames.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone1.pngbin1047 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone2.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone3.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone4.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fingers_crossed_tone5.pngbin1050 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fire.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fire_engine.pngbin1656 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fireworks.pngbin1364 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_place.pngbin1419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon.pngbin1152 -> 0 bytes
-rw-r--r--app/assets/images/emoji/first_quarter_moon_with_face.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fish.pngbin1080 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fish_cake.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fishing_pole_and_fish.pngbin1442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone1.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone2.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone3.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone4.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fist_tone5.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/five.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ac.pngbin1934 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ad.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ae.pngbin544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_af.pngbin942 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ag.pngbin913 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ai.pngbin1056 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_al.pngbin905 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_am.pngbin514 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ao.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_aq.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ar.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_as.pngbin1489 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_at.pngbin430 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_au.pngbin962 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_aw.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ax.pngbin496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_az.pngbin709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ba.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bb.pngbin789 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bd.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_be.pngbin444 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bf.pngbin717 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bg.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bh.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bi.pngbin795 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bj.pngbin554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bl.pngbin1691 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_black.pngbin702 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bm.pngbin1374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bn.pngbin1355 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bo.pngbin1132 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bq.pngbin1144 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_br.pngbin819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bs.pngbin448 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bt.pngbin1213 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bv.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bw.pngbin391 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_by.pngbin1120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_bz.pngbin1595 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ca.pngbin755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cc.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cd.pngbin707 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cf.pngbin673 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cg.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ch.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ci.pngbin440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ck.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cl.pngbin748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cm.pngbin627 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cn.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_co.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cp.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cr.pngbin419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cu.pngbin586 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cv.pngbin642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cw.pngbin665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cx.pngbin1142 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cy.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_cz.pngbin600 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_de.pngbin502 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dg.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dj.pngbin753 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dk.pngbin450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dm.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_do.pngbin1135 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_dz.pngbin734 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ea.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ec.pngbin1431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ee.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eg.pngbin818 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eh.pngbin742 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_er.pngbin1218 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_es.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_et.pngbin947 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_eu.pngbin760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fi.pngbin487 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fj.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fk.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fm.pngbin554 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fo.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_fr.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ga.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gb.pngbin919 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gd.pngbin1017 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ge.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gf.pngbin865 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gg.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gh.pngbin723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gi.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gl.pngbin700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gm.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gn.pngbin434 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gp.pngbin1587 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gq.pngbin1132 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gr.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gs.pngbin2115 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gt.pngbin1087 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gu.pngbin1045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gw.pngbin705 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_gy.pngbin690 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hk.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hm.pngbin1036 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hn.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hr.pngbin1411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ht.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_hu.pngbin513 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ic.pngbin1330 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_id.pngbin498 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ie.pngbin478 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_il.pngbin658 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_im.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_in.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_io.pngbin1911 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_iq.pngbin811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ir.pngbin1036 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_is.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_it.pngbin472 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_je.pngbin956 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jm.pngbin837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jo.pngbin740 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_jp.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ke.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kg.pngbin1080 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kh.pngbin872 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ki.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_km.pngbin783 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kn.pngbin1316 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kp.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kr.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kw.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ky.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_kz.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_la.pngbin479 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lb.pngbin740 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lc.pngbin561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_li.pngbin946 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lk.pngbin974 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lr.pngbin772 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ls.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lt.pngbin510 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lu.pngbin512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_lv.pngbin388 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ly.pngbin685 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ma.pngbin626 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mc.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_md.pngbin1170 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_me.pngbin1074 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mf.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mg.pngbin556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mh.pngbin1138 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mk.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ml.pngbin440 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mm.pngbin937 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mn.pngbin698 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mo.pngbin792 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mp.pngbin1797 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mq.pngbin780 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mr.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ms.pngbin1477 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mt.pngbin799 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mu.pngbin544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mv.pngbin598 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mw.pngbin825 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mx.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_my.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_mz.pngbin1159 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_na.pngbin1249 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nc.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ne.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nf.pngbin877 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ng.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ni.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nl.pngbin499 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_no.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_np.pngbin802 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nr.pngbin529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nu.pngbin1128 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_nz.pngbin1099 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_om.pngbin754 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pa.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pe.pngbin439 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pf.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pg.pngbin1076 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ph.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pk.pngbin753 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pl.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pm.pngbin2314 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pn.pngbin1895 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pr.pngbin605 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ps.pngbin574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pt.pngbin1055 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_pw.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_py.pngbin1085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_qa.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_re.pngbin837 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ro.pngbin441 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_rs.pngbin1237 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ru.pngbin496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_rw.pngbin940 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sa.pngbin781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sb.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sc.pngbin1073 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sd.pngbin578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_se.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sg.pngbin730 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sh.pngbin1369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_si.pngbin1030 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sj.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sk.pngbin780 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sl.pngbin510 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sm.pngbin2000 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sn.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_so.pngbin609 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sr.pngbin650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ss.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_st.pngbin562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sv.pngbin1125 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sx.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sy.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_sz.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ta.pngbin1907 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tc.pngbin1538 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_td.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tf.pngbin857 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tg.pngbin790 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_th.pngbin421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tj.pngbin906 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tk.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tl.pngbin849 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tm.pngbin1178 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tn.pngbin625 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_to.pngbin553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tr.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tt.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tv.pngbin1120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tw.pngbin761 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_tz.pngbin1061 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ua.pngbin528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ug.pngbin887 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_um.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_us.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_uy.pngbin966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_uz.pngbin750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_va.pngbin1331 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vc.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ve.pngbin748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vg.pngbin1789 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vi.pngbin1378 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vn.pngbin583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_vu.pngbin844 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_wf.pngbin443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_white.pngbin699 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ws.pngbin634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_xk.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_ye.pngbin507 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_yt.pngbin1623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_za.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_zm.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flag_zw.pngbin993 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flags.pngbin1722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flashlight.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fleur-de-lis.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/floppy_disk.pngbin258 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flower_playing_cards.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/flushed.pngbin1127 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fog.pngbin713 -> 0 bytes
-rw-r--r--app/assets/images/emoji/foggy.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/football.pngbin956 -> 0 bytes
-rw-r--r--app/assets/images/emoji/footprints.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fork_and_knife.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fork_knife_plate.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fountain.pngbin1768 -> 0 bytes
-rw-r--r--app/assets/images/emoji/four.pngbin497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/four_leaf_clover.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fox.pngbin1556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frame_photo.pngbin514 -> 0 bytes
-rw-r--r--app/assets/images/emoji/free.pngbin370 -> 0 bytes
-rw-r--r--app/assets/images/emoji/french_bread.pngbin1551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fried_shrimp.pngbin1241 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fries.pngbin1873 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frog.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frowning.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/frowning2.pngbin589 -> 0 bytes
-rw-r--r--app/assets/images/emoji/fuelpump.pngbin864 -> 0 bytes
-rw-r--r--app/assets/images/emoji/full_moon.pngbin841 -> 0 bytes
-rw-r--r--app/assets/images/emoji/full_moon_with_face.pngbin1186 -> 0 bytes
-rw-r--r--app/assets/images/emoji/game_die.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gay_pride_flag.pngbin2340 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gear.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gem.pngbin715 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gemini.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ghost.pngbin1465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gift.pngbin1966 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gift_heart.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl.pngbin1261 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone1.pngbin1259 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone2.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone3.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone4.pngbin1241 -> 0 bytes
-rw-r--r--app/assets/images/emoji/girl_tone5.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/globe_with_meridians.pngbin796 -> 0 bytes
-rw-r--r--app/assets/images/emoji/goal.pngbin1242 -> 0 bytes
-rw-r--r--app/assets/images/emoji/goat.pngbin981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/golf.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/golfer.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gorilla.pngbin1090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grapes.pngbin1552 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_apple.pngbin656 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_book.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/green_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grey_exclamation.pngbin354 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grey_question.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grimacing.pngbin694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grin.pngbin767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/grinning.pngbin810 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman.pngbin1140 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone1.pngbin1122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone2.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone3.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone4.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guardsman_tone5.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/guitar.pngbin1056 -> 0 bytes
-rw-r--r--app/assets/images/emoji/gun.pngbin1859 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut.pngbin1935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone1.pngbin1945 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone2.pngbin1935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone3.pngbin1923 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone4.pngbin1904 -> 0 bytes
-rw-r--r--app/assets/images/emoji/haircut_tone5.pngbin1920 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hamburger.pngbin1973 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hammer.pngbin834 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hammer_pick.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hamster.pngbin1279 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone1.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone2.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone3.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone4.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hand_splayed_tone5.pngbin1081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handbag.pngbin1285 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone1.pngbin1645 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone2.pngbin1628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone3.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone4.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handball_tone5.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone1.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone2.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone3.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone4.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/handshake_tone5.pngbin1381 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hash.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hatched_chick.pngbin1174 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hatching_chick.pngbin1598 -> 0 bytes
-rw-r--r--app/assets/images/emoji/head_bandage.pngbin1199 -> 0 bytes
-rw-r--r--app/assets/images/emoji/headphones.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hear_no_evil.pngbin1210 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_decoration.pngbin557 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_exclamation.pngbin471 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heart_eyes_cat.pngbin1512 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heartbeat.pngbin699 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heartpulse.pngbin675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hearts.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_check_mark.pngbin438 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_division_sign.pngbin204 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_dollar_sign.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_minus_sign.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_multiplication_x.pngbin298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/heavy_plus_sign.pngbin115 -> 0 bytes
-rw-r--r--app/assets/images/emoji/helicopter.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/helmet_with_cross.pngbin1014 -> 0 bytes
-rw-r--r--app/assets/images/emoji/herb.pngbin886 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hibiscus.pngbin1815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/high_brightness.pngbin474 -> 0 bytes
-rw-r--r--app/assets/images/emoji/high_heel.pngbin1008 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hockey.pngbin1010 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hole.pngbin1390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/homes.pngbin981 -> 0 bytes
-rw-r--r--app/assets/images/emoji/honey_pot.pngbin1217 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse.pngbin1694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing.pngbin2096 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone1.pngbin2099 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone2.pngbin2103 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone3.pngbin2090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone4.pngbin2090 -> 0 bytes
-rw-r--r--app/assets/images/emoji/horse_racing_tone5.pngbin2085 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hospital.pngbin530 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hot_pepper.pngbin677 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotdog.pngbin1770 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotel.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hotsprings.pngbin733 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hourglass.pngbin800 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hourglass_flowing_sand.pngbin847 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house_abandoned.pngbin1606 -> 0 bytes
-rw-r--r--app/assets/images/emoji/house_with_garden.pngbin1613 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hugging.pngbin1425 -> 0 bytes
-rw-r--r--app/assets/images/emoji/hushed.pngbin634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ice_cream.pngbin1779 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ice_skate.pngbin1574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/icecream.pngbin1496 -> 0 bytes
-rw-r--r--app/assets/images/emoji/id.pngbin348 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ideograph_advantage.pngbin716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/imp.pngbin1988 -> 0 bytes
-rw-r--r--app/assets/images/emoji/inbox_tray.pngbin1029 -> 0 bytes
-rw-r--r--app/assets/images/emoji/incoming_envelope.pngbin1129 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person.pngbin1580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone1.pngbin1597 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone2.pngbin1590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone3.pngbin1580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone4.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_desk_person_tone5.pngbin1588 -> 0 bytes
-rw-r--r--app/assets/images/emoji/information_source.pngbin506 -> 0 bytes
-rw-r--r--app/assets/images/emoji/innocent.pngbin935 -> 0 bytes
-rw-r--r--app/assets/images/emoji/interrobang.pngbin601 -> 0 bytes
-rw-r--r--app/assets/images/emoji/iphone.pngbin695 -> 0 bytes
-rw-r--r--app/assets/images/emoji/island.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/izakaya_lantern.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/jack_o_lantern.pngbin2289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japan.pngbin539 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_castle.pngbin1404 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_goblin.pngbin1561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/japanese_ogre.pngbin1864 -> 0 bytes
-rw-r--r--app/assets/images/emoji/jeans.pngbin1158 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joy.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joy_cat.pngbin1633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/joystick.pngbin1039 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone1.pngbin1171 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone2.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone3.pngbin1170 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone4.pngbin1167 -> 0 bytes
-rw-r--r--app/assets/images/emoji/juggling_tone5.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kaaba.pngbin1251 -> 0 bytes
-rw-r--r--app/assets/images/emoji/key.pngbin770 -> 0 bytes
-rw-r--r--app/assets/images/emoji/key2.pngbin593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/keyboard.pngbin429 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kimono.pngbin1527 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss.pngbin842 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss_mm.pngbin1269 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiss_ww.pngbin1149 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing.pngbin738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_cat.pngbin1468 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_closed_eyes.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_heart.pngbin843 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kissing_smiling_eyes.pngbin648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/kiwi.pngbin1892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/knife.pngbin616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/koala.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/koko.pngbin266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/label.pngbin669 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_blue_circle.pngbin371 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_blue_diamond.pngbin245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/large_orange_diamond.pngbin248 -> 0 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon.pngbin1180 -> 0 bytes
-rw-r--r--app/assets/images/emoji/last_quarter_moon_with_face.pngbin1030 -> 0 bytes
-rw-r--r--app/assets/images/emoji/laughing.pngbin901 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leaves.pngbin993 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ledger.pngbin1528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist.pngbin972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone1.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone2.pngbin972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone3.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone4.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_facing_fist_tone5.pngbin976 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_luggage.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/left_right_arrow.pngbin495 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leftwards_arrow_with_hook.pngbin643 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lemon.pngbin1033 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leo.pngbin745 -> 0 bytes
-rw-r--r--app/assets/images/emoji/leopard.pngbin2222 -> 0 bytes
-rw-r--r--app/assets/images/emoji/level_slider.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/levitate.pngbin914 -> 0 bytes
-rw-r--r--app/assets/images/emoji/libra.pngbin657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter.pngbin1356 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone1.pngbin1346 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone2.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone3.pngbin1339 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone4.pngbin1343 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lifter_tone5.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/light_rail.pngbin902 -> 0 bytes
-rw-r--r--app/assets/images/emoji/link.pngbin477 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lion_face.pngbin1728 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lips.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lipstick.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lizard.pngbin1709 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lock.pngbin986 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lock_with_ink_pen.pngbin1123 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lollipop.pngbin2164 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loop.pngbin550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loud_sound.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/loudspeaker.pngbin1316 -> 0 bytes
-rw-r--r--app/assets/images/emoji/love_hotel.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/love_letter.pngbin923 -> 0 bytes
-rw-r--r--app/assets/images/emoji/low_brightness.pngbin431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/lying_face.pngbin1103 -> 0 bytes
-rw-r--r--app/assets/images/emoji/m.pngbin500 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mag.pngbin1240 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mag_right.pngbin1251 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mahjong.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox.pngbin1166 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_closed.pngbin1192 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_mail.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mailbox_with_no_mail.pngbin960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man.pngbin1092 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing.pngbin1400 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone1.pngbin1404 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone2.pngbin1402 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone3.pngbin1409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone4.pngbin1421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_dancing_tone5.pngbin1418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone1.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone2.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone3.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone4.pngbin1307 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_in_tuxedo_tone5.pngbin1302 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone1.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone2.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone3.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone4.pngbin1069 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_tone5.pngbin1087 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao.pngbin1339 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone1.pngbin1328 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone2.pngbin1332 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone3.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone4.pngbin1325 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_gua_pi_mao_tone5.pngbin1337 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone1.pngbin1584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone2.pngbin1588 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone3.pngbin1584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone4.pngbin1583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/man_with_turban_tone5.pngbin1605 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mans_shoe.pngbin1649 -> 0 bytes
-rw-r--r--app/assets/images/emoji/map.pngbin2352 -> 0 bytes
-rw-r--r--app/assets/images/emoji/maple_leaf.pngbin1117 -> 0 bytes
-rw-r--r--app/assets/images/emoji/martial_arts_uniform.pngbin1412 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mask.pngbin1322 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage.pngbin1571 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone1.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone2.pngbin1565 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone3.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone4.pngbin1546 -> 0 bytes
-rw-r--r--app/assets/images/emoji/massage_tone5.pngbin1557 -> 0 bytes
-rw-r--r--app/assets/images/emoji/meat_on_bone.pngbin1465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/medal.pngbin1700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mega.pngbin1751 -> 0 bytes
-rw-r--r--app/assets/images/emoji/melon.pngbin2005 -> 0 bytes
-rw-r--r--app/assets/images/emoji/menorah.pngbin1279 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mens.pngbin561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone1.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone2.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone3.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone4.pngbin888 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metal_tone5.pngbin894 -> 0 bytes
-rw-r--r--app/assets/images/emoji/metro.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microphone.pngbin1165 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microphone2.pngbin839 -> 0 bytes
-rw-r--r--app/assets/images/emoji/microscope.pngbin1113 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger.pngbin893 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone1.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone2.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone3.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone4.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/middle_finger_tone5.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/military_medal.pngbin949 -> 0 bytes
-rw-r--r--app/assets/images/emoji/milk.pngbin1224 -> 0 bytes
-rw-r--r--app/assets/images/emoji/milky_way.pngbin622 -> 0 bytes
-rw-r--r--app/assets/images/emoji/minibus.pngbin1256 -> 0 bytes
-rw-r--r--app/assets/images/emoji/minidisc.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mobile_phone_off.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/money_mouth.pngbin967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/money_with_wings.pngbin2327 -> 0 bytes
-rw-r--r--app/assets/images/emoji/moneybag.pngbin2310 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monkey.pngbin1348 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monkey_face.pngbin1022 -> 0 bytes
-rw-r--r--app/assets/images/emoji/monorail.pngbin1068 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mortar_board.pngbin710 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mosque.pngbin984 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motor_scooter.pngbin1207 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorboat.pngbin990 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorcycle.pngbin2081 -> 0 bytes
-rw-r--r--app/assets/images/emoji/motorway.pngbin1102 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mount_fuji.pngbin881 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain.pngbin1409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist.pngbin2288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone1.pngbin2294 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone2.pngbin2298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone3.pngbin2284 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone4.pngbin2288 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_bicyclist_tone5.pngbin2281 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_cableway.pngbin811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_railway.pngbin1317 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mountain_snow.pngbin1193 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse.pngbin1245 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse2.pngbin1324 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mouse_three_button.pngbin934 -> 0 bytes
-rw-r--r--app/assets/images/emoji/movie_camera.pngbin576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/moyai.pngbin1593 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus.pngbin3338 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone1.pngbin1999 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone2.pngbin2006 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone3.pngbin2017 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone4.pngbin2016 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus_tone5.pngbin2016 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone1.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone2.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone3.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone4.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/muscle_tone5.pngbin1012 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mushroom.pngbin1024 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_keyboard.pngbin1695 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_note.pngbin419 -> 0 bytes
-rw-r--r--app/assets/images/emoji/musical_score.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/mute.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone1.pngbin1712 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone2.pngbin1711 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone3.pngbin1727 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone4.pngbin1728 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nail_care_tone5.pngbin1716 -> 0 bytes
-rw-r--r--app/assets/images/emoji/name_badge.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nauseated_face.pngbin965 -> 0 bytes
-rw-r--r--app/assets/images/emoji/necktie.pngbin995 -> 0 bytes
-rw-r--r--app/assets/images/emoji/negative_squared_cross_mark.pngbin370 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nerd.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/neutral_face.pngbin517 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new.pngbin486 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new_moon.pngbin829 -> 0 bytes
-rw-r--r--app/assets/images/emoji/new_moon_with_face.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/newspaper.pngbin1178 -> 0 bytes
-rw-r--r--app/assets/images/emoji/newspaper2.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ng.pngbin445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/night_with_stars.pngbin835 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nine.pngbin607 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_bell.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_bicycles.pngbin998 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_entry.pngbin377 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_entry_sign.pngbin555 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good.pngbin1750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone1.pngbin1767 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone2.pngbin1756 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone3.pngbin1766 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone4.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_good_tone5.pngbin1784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_mobile_phones.pngbin790 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_mouth.pngbin465 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_pedestrians.pngbin875 -> 0 bytes
-rw-r--r--app/assets/images/emoji/no_smoking.pngbin1136 -> 0 bytes
-rw-r--r--app/assets/images/emoji/non-potable_water.pngbin827 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone1.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone2.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone3.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone4.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nose_tone5.pngbin703 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notebook.pngbin1215 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notebook_with_decorative_cover.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notepad_spiral.pngbin1377 -> 0 bytes
-rw-r--r--app/assets/images/emoji/notes.pngbin501 -> 0 bytes
-rw-r--r--app/assets/images/emoji/nut_and_bolt.pngbin899 -> 0 bytes
-rw-r--r--app/assets/images/emoji/o.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/o2.pngbin425 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ocean.pngbin1018 -> 0 bytes
-rw-r--r--app/assets/images/emoji/octagonal_sign.pngbin260 -> 0 bytes
-rw-r--r--app/assets/images/emoji/octopus.pngbin1188 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oden.pngbin794 -> 0 bytes
-rw-r--r--app/assets/images/emoji/office.pngbin524 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oil.pngbin674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok.pngbin511 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone1.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone2.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone3.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone4.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_hand_tone5.pngbin979 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone1.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone2.pngbin1694 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone3.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone4.pngbin1684 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ok_woman_tone5.pngbin1696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone1.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone2.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone3.pngbin1253 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone4.pngbin1254 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_man_tone5.pngbin1254 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman.pngbin1472 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone1.pngbin1562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone2.pngbin1564 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone3.pngbin1555 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone4.pngbin1562 -> 0 bytes
-rw-r--r--app/assets/images/emoji/older_woman_tone5.pngbin1544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/om_symbol.pngbin773 -> 0 bytes
-rw-r--r--app/assets/images/emoji/on.pngbin459 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_automobile.pngbin1238 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_bus.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_police_car.pngbin1547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/oncoming_taxi.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/one.pngbin442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_file_folder.pngbin755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone1.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone2.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone3.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone4.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_hands_tone5.pngbin1053 -> 0 bytes
-rw-r--r--app/assets/images/emoji/open_mouth.pngbin575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ophiuchus.pngbin723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/orange_book.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/orthodox_cross.pngbin239 -> 0 bytes
-rw-r--r--app/assets/images/emoji/outbox_tray.pngbin1002 -> 0 bytes
-rw-r--r--app/assets/images/emoji/owl.pngbin2045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ox.pngbin1436 -> 0 bytes
-rw-r--r--app/assets/images/emoji/package.pngbin950 -> 0 bytes
-rw-r--r--app/assets/images/emoji/page_facing_up.pngbin1110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/page_with_curl.pngbin1157 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pager.pngbin553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paintbrush.pngbin950 -> 0 bytes
-rw-r--r--app/assets/images/emoji/palm_tree.pngbin1450 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pancakes.pngbin3661 -> 0 bytes
-rw-r--r--app/assets/images/emoji/panda_face.pngbin1478 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paperclip.pngbin439 -> 0 bytes
-rw-r--r--app/assets/images/emoji/paperclips.pngbin642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/park.pngbin929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/parking.pngbin385 -> 0 bytes
-rw-r--r--app/assets/images/emoji/part_alternation_mark.pngbin521 -> 0 bytes
-rw-r--r--app/assets/images/emoji/partly_sunny.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/passport_control.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pause_button.pngbin395 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peace.pngbin933 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peach.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/peanuts.pngbin3266 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pear.pngbin747 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pen_ballpoint.pngbin696 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pen_fountain.pngbin623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pencil.pngbin1624 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pencil2.pngbin654 -> 0 bytes
-rw-r--r--app/assets/images/emoji/penguin.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pensive.pngbin718 -> 0 bytes
-rw-r--r--app/assets/images/emoji/performing_arts.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/persevere.pngbin891 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone1.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone2.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone3.pngbin1141 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone4.pngbin1109 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_frowning_tone5.pngbin1114 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair.pngbin1205 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone1.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone2.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone3.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone4.pngbin1189 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_blond_hair_tone5.pngbin1214 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face.pngbin1297 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone1.pngbin1309 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone2.pngbin1292 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone3.pngbin1305 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone4.pngbin1296 -> 0 bytes
-rw-r--r--app/assets/images/emoji/person_with_pouting_face_tone5.pngbin1303 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pick.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig.pngbin1138 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig2.pngbin1548 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pig_nose.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pill.pngbin442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pineapple.pngbin1642 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ping_pong.pngbin823 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pisces.pngbin678 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pizza.pngbin2008 -> 0 bytes
-rw-r--r--app/assets/images/emoji/place_of_worship.pngbin487 -> 0 bytes
-rw-r--r--app/assets/images/emoji/play_pause.pngbin509 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down.pngbin853 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone1.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone2.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone3.pngbin858 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone4.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_down_tone5.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left.pngbin825 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone1.pngbin832 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone2.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone3.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone4.pngbin830 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_left_tone5.pngbin832 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone1.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone2.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone3.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone4.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_right_tone5.pngbin805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up.pngbin819 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone1.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone2.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone3.pngbin871 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone4.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_2_tone5.pngbin822 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone1.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone2.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone3.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone4.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/point_up_tone5.pngbin820 -> 0 bytes
-rw-r--r--app/assets/images/emoji/police_car.pngbin1431 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poodle.pngbin1531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poop.pngbin1273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/popcorn.pngbin1843 -> 0 bytes
-rw-r--r--app/assets/images/emoji/post_office.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/postal_horn.pngbin809 -> 0 bytes
-rw-r--r--app/assets/images/emoji/postbox.pngbin1077 -> 0 bytes
-rw-r--r--app/assets/images/emoji/potable_water.pngbin633 -> 0 bytes
-rw-r--r--app/assets/images/emoji/potato.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pouch.pngbin1259 -> 0 bytes
-rw-r--r--app/assets/images/emoji/poultry_leg.pngbin925 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pound.pngbin452 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pouting_cat.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray.pngbin1122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone1.pngbin1131 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone2.pngbin1134 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone3.pngbin1137 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone4.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pray_tone5.pngbin1117 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prayer_beads.pngbin1059 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman.pngbin1252 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone1.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone2.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone3.pngbin1237 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone4.pngbin1246 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pregnant_woman_tone5.pngbin1235 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince.pngbin1616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone1.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone2.pngbin1621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone3.pngbin1619 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone4.pngbin1619 -> 0 bytes
-rw-r--r--app/assets/images/emoji/prince_tone5.pngbin1616 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone1.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone2.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone3.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone4.pngbin1813 -> 0 bytes
-rw-r--r--app/assets/images/emoji/princess_tone5.pngbin1812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/printer.pngbin926 -> 0 bytes
-rw-r--r--app/assets/images/emoji/projector.pngbin943 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone1.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone2.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone3.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone4.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/punch_tone5.pngbin838 -> 0 bytes
-rw-r--r--app/assets/images/emoji/purple_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/purse.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/pushpin.pngbin640 -> 0 bytes
-rw-r--r--app/assets/images/emoji/put_litter_in_its_place.pngbin650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/question.pngbin449 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rabbit.pngbin1660 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rabbit2.pngbin1805 -> 0 bytes
-rw-r--r--app/assets/images/emoji/race_car.pngbin2140 -> 0 bytes
-rw-r--r--app/assets/images/emoji/racehorse.pngbin1401 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radio.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radio_button.pngbin674 -> 0 bytes
-rw-r--r--app/assets/images/emoji/radioactive.pngbin858 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rage.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/railway_car.pngbin847 -> 0 bytes
-rw-r--r--app/assets/images/emoji/railway_track.pngbin1550 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rainbow.pngbin1299 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone1.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone2.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone3.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone4.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_back_of_hand_tone5.pngbin848 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone1.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone2.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone3.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone4.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hand_tone5.pngbin791 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone1.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone2.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone3.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone4.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raised_hands_tone5.pngbin1098 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand.pngbin1664 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone1.pngbin1678 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone2.pngbin1665 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone3.pngbin1657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone4.pngbin1657 -> 0 bytes
-rw-r--r--app/assets/images/emoji/raising_hand_tone5.pngbin1661 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ram.pngbin1951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ramen.pngbin1992 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rat.pngbin1193 -> 0 bytes
-rw-r--r--app/assets/images/emoji/record_button.pngbin475 -> 0 bytes
-rw-r--r--app/assets/images/emoji/recycle.pngbin914 -> 0 bytes
-rw-r--r--app/assets/images/emoji/red_car.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/red_circle.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/registered.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/relaxed.pngbin636 -> 0 bytes
-rw-r--r--app/assets/images/emoji/relieved.pngbin785 -> 0 bytes
-rw-r--r--app/assets/images/emoji/reminder_ribbon.pngbin921 -> 0 bytes
-rw-r--r--app/assets/images/emoji/repeat.pngbin644 -> 0 bytes
-rw-r--r--app/assets/images/emoji/repeat_one.pngbin688 -> 0 bytes
-rw-r--r--app/assets/images/emoji/restroom.pngbin676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/revolving_hearts.pngbin920 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rewind.pngbin523 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rhino.pngbin1558 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ribbon.pngbin968 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_ball.pngbin1091 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_cracker.pngbin1443 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rice_scene.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist.pngbin975 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone1.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone2.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone3.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone4.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/right_facing_fist_tone5.pngbin964 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ring.pngbin1113 -> 0 bytes
-rw-r--r--app/assets/images/emoji/robot.pngbin1228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rocket.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rofl.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/roller_coaster.pngbin1723 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rolling_eyes.pngbin743 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rooster.pngbin1333 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rose.pngbin1182 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rosette.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rotating_light.pngbin1969 -> 0 bytes
-rw-r--r--app/assets/images/emoji/round_pushpin.pngbin455 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat.pngbin1963 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone1.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone2.pngbin1972 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone3.pngbin1967 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone4.pngbin1974 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rowboat_tone5.pngbin1971 -> 0 bytes
-rw-r--r--app/assets/images/emoji/rugby_football.pngbin1618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone1.pngbin1163 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone2.pngbin1162 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone3.pngbin1151 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone4.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/runner_tone5.pngbin1145 -> 0 bytes
-rw-r--r--app/assets/images/emoji/running_shirt_with_sash.pngbin784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sa.pngbin420 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sagittarius.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sailboat.pngbin1274 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sake.pngbin826 -> 0 bytes
-rw-r--r--app/assets/images/emoji/salad.pngbin2398 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sandal.pngbin1180 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa.pngbin1585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone1.pngbin1585 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone2.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone3.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone4.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/santa_tone5.pngbin1578 -> 0 bytes
-rw-r--r--app/assets/images/emoji/satellite.pngbin1173 -> 0 bytes
-rw-r--r--app/assets/images/emoji/satellite_orbital.pngbin762 -> 0 bytes
-rw-r--r--app/assets/images/emoji/saxophone.pngbin1442 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scales.pngbin1181 -> 0 bytes
-rw-r--r--app/assets/images/emoji/school.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/school_satchel.pngbin1490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scissors.pngbin937 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scooter.pngbin1228 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scorpion.pngbin1503 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scorpius.pngbin612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scream.pngbin1583 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scream_cat.pngbin2120 -> 0 bytes
-rw-r--r--app/assets/images/emoji/scroll.pngbin989 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seat.pngbin884 -> 0 bytes
-rw-r--r--app/assets/images/emoji/second_place.pngbin1511 -> 0 bytes
-rw-r--r--app/assets/images/emoji/secret.pngbin857 -> 0 bytes
-rw-r--r--app/assets/images/emoji/see_no_evil.pngbin1227 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seedling.pngbin749 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie.pngbin1160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone1.pngbin1166 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone2.pngbin1167 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone3.pngbin1154 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone4.pngbin1153 -> 0 bytes
-rw-r--r--app/assets/images/emoji/selfie_tone5.pngbin1148 -> 0 bytes
-rw-r--r--app/assets/images/emoji/seven.pngbin522 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shallow_pan_of_food.pngbin1738 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shamrock.pngbin1023 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shark.pngbin1811 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shaved_ice.pngbin997 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sheep.pngbin1372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shell.pngbin1497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shield.pngbin1602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shinto_shrine.pngbin579 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ship.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shirt.pngbin670 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shopping_bags.pngbin1234 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shopping_cart.pngbin1072 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shower.pngbin2537 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrimp.pngbin1376 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone1.pngbin1676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone2.pngbin1671 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone3.pngbin1675 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone4.pngbin1641 -> 0 bytes
-rw-r--r--app/assets/images/emoji/shrug_tone5.pngbin1634 -> 0 bytes
-rw-r--r--app/assets/images/emoji/signal_strength.pngbin445 -> 0 bytes
-rw-r--r--app/assets/images/emoji/six.pngbin612 -> 0 bytes
-rw-r--r--app/assets/images/emoji/six_pointed_star.pngbin540 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ski.pngbin1762 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skier.pngbin1539 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skull.pngbin628 -> 0 bytes
-rw-r--r--app/assets/images/emoji/skull_crossbones.pngbin726 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleeping.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleeping_accommodation.pngbin926 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sleepy.pngbin1185 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slight_frown.pngbin580 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slight_smile.pngbin600 -> 0 bytes
-rw-r--r--app/assets/images/emoji/slot_machine.pngbin1648 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_blue_diamond.pngbin191 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_orange_diamond.pngbin194 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle.pngbin273 -> 0 bytes
-rw-r--r--app/assets/images/emoji/small_red_triangle_down.pngbin291 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smile.pngbin737 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smile_cat.pngbin1405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiley.pngbin686 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiley_cat.pngbin1669 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smiling_imp.pngbin1078 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smirk.pngbin775 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smirk_cat.pngbin1663 -> 0 bytes
-rw-r--r--app/assets/images/emoji/smoking.pngbin417 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snail.pngbin1731 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snake.pngbin1575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sneezing_face.pngbin1289 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowboarder.pngbin2020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowflake.pngbin691 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowman.pngbin1481 -> 0 bytes
-rw-r--r--app/assets/images/emoji/snowman2.pngbin2176 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sob.pngbin1236 -> 0 bytes
-rw-r--r--app/assets/images/emoji/soccer.pngbin1034 -> 0 bytes
-rw-r--r--app/assets/images/emoji/soon.pngbin483 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sos.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sound.pngbin690 -> 0 bytes
-rw-r--r--app/assets/images/emoji/space_invader.pngbin1325 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spades.pngbin454 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spaghetti.pngbin1796 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkle.pngbin663 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkler.pngbin910 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkles.pngbin651 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sparkling_heart.pngbin821 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speak_no_evil.pngbin1497 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speaker.pngbin575 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speaking_head.pngbin531 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speech_balloon.pngbin384 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speech_left.pngbin390 -> 0 bytes
-rw-r--r--app/assets/images/emoji/speedboat.pngbin1255 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spider.pngbin1724 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spider_web.pngbin929 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spoon.pngbin700 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy.pngbin1650 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone1.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone2.pngbin1632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone3.pngbin1645 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone4.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/spy_tone5.pngbin1639 -> 0 bytes
-rw-r--r--app/assets/images/emoji/squid.pngbin1394 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stadium.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star.pngbin456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star2.pngbin732 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star_and_crescent.pngbin490 -> 0 bytes
-rw-r--r--app/assets/images/emoji/star_of_david.pngbin491 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stars.pngbin1048 -> 0 bytes
-rw-r--r--app/assets/images/emoji/station.pngbin1336 -> 0 bytes
-rw-r--r--app/assets/images/emoji/statue_of_liberty.pngbin1145 -> 0 bytes
-rw-r--r--app/assets/images/emoji/steam_locomotive.pngbin1736 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stew.pngbin1960 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stop_button.pngbin385 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stopwatch.pngbin1329 -> 0 bytes
-rw-r--r--app/assets/images/emoji/straight_ruler.pngbin1406 -> 0 bytes
-rw-r--r--app/assets/images/emoji/strawberry.pngbin1206 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_closed_eyes.pngbin867 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuck_out_tongue_winking_eye.pngbin1061 -> 0 bytes
-rw-r--r--app/assets/images/emoji/stuffed_flatbread.pngbin2160 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sun_with_face.pngbin741 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunflower.pngbin1915 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunglasses.pngbin824 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunny.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunrise.pngbin812 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sunrise_over_mountains.pngbin1576 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer.pngbin1777 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone1.pngbin1781 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone2.pngbin1769 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone3.pngbin1777 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone4.pngbin1784 -> 0 bytes
-rw-r--r--app/assets/images/emoji/surfer_tone5.pngbin1782 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sushi.pngbin2101 -> 0 bytes
-rw-r--r--app/assets/images/emoji/suspension_railway.pngbin927 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat.pngbin861 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat_drops.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweat_smile.pngbin851 -> 0 bytes
-rw-r--r--app/assets/images/emoji/sweet_potato.pngbin951 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone1.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone2.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone3.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone4.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/swimmer_tone5.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/symbols.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/synagogue.pngbin1309 -> 0 bytes
-rw-r--r--app/assets/images/emoji/syringe.pngbin737 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taco.pngbin3045 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tada.pngbin1778 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tanabata_tree.pngbin1479 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tangerine.pngbin1184 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taurus.pngbin701 -> 0 bytes
-rw-r--r--app/assets/images/emoji/taxi.pngbin1230 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tea.pngbin1297 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telephone.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telephone_receiver.pngbin941 -> 0 bytes
-rw-r--r--app/assets/images/emoji/telescope.pngbin1256 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ten.pngbin621 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tennis.pngbin1561 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tent.pngbin1684 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thermometer.pngbin759 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thermometer_face.pngbin1503 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thinking.pngbin1345 -> 0 bytes
-rw-r--r--app/assets/images/emoji/third_place.pngbin1529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thought_balloon.pngbin489 -> 0 bytes
-rw-r--r--app/assets/images/emoji/three.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone1.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone2.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone3.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone4.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsdown_tone5.pngbin815 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone1.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone2.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone3.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone4.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thumbsup_tone5.pngbin814 -> 0 bytes
-rw-r--r--app/assets/images/emoji/thunder_cloud_rain.pngbin1020 -> 0 bytes
-rw-r--r--app/assets/images/emoji/ticket.pngbin763 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tickets.pngbin1750 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tiger.pngbin2104 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tiger2.pngbin2623 -> 0 bytes
-rw-r--r--app/assets/images/emoji/timer.pngbin1897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tired_face.pngbin1126 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tm.pngbin300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/toilet.pngbin726 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tokyo_tower.pngbin765 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tomato.pngbin1055 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone1.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone2.pngbin372 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone3.pngbin375 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone4.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tone5.pngbin374 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tongue.pngbin599 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tools.pngbin1225 -> 0 bytes
-rw-r--r--app/assets/images/emoji/top.pngbin389 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tophat.pngbin845 -> 0 bytes
-rw-r--r--app/assets/images/emoji/track_next.pngbin551 -> 0 bytes
-rw-r--r--app/assets/images/emoji/track_previous.pngbin549 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trackball.pngbin892 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tractor.pngbin1192 -> 0 bytes
-rw-r--r--app/assets/images/emoji/traffic_light.pngbin590 -> 0 bytes
-rw-r--r--app/assets/images/emoji/train.pngbin1031 -> 0 bytes
-rw-r--r--app/assets/images/emoji/train2.pngbin1499 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tram.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triangular_flag_on_post.pngbin415 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triangular_ruler.pngbin369 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trident.pngbin668 -> 0 bytes
-rw-r--r--app/assets/images/emoji/triumph.pngbin1529 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trolleybus.pngbin1168 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trophy.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tropical_drink.pngbin1428 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tropical_fish.pngbin1676 -> 0 bytes
-rw-r--r--app/assets/images/emoji/truck.pngbin1366 -> 0 bytes
-rw-r--r--app/assets/images/emoji/trumpet.pngbin1281 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tulip.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tumbler_glass.pngbin2312 -> 0 bytes
-rw-r--r--app/assets/images/emoji/turkey.pngbin1240 -> 0 bytes
-rw-r--r--app/assets/images/emoji/turtle.pngbin1515 -> 0 bytes
-rw-r--r--app/assets/images/emoji/tv.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/twisted_rightwards_arrows.pngbin574 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two.pngbin567 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_hearts.pngbin493 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_men_holding_hands.pngbin1347 -> 0 bytes
-rw-r--r--app/assets/images/emoji/two_women_holding_hands.pngbin1544 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u5272.pngbin411 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u5408.pngbin484 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u55b6.pngbin460 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6307.pngbin504 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6708.pngbin409 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6709.pngbin434 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u6e80.pngbin564 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7121.pngbin534 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7533.pngbin306 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7981.pngbin584 -> 0 bytes
-rw-r--r--app/assets/images/emoji/u7a7a.pngbin456 -> 0 bytes
-rw-r--r--app/assets/images/emoji/umbrella.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/umbrella2.pngbin897 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unamused.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/underage.pngbin863 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unicorn.pngbin2107 -> 0 bytes
-rw-r--r--app/assets/images/emoji/unlock.pngbin856 -> 0 bytes
-rw-r--r--app/assets/images/emoji/up.pngbin405 -> 0 bytes
-rw-r--r--app/assets/images/emoji/upside_down.pngbin602 -> 0 bytes
-rw-r--r--app/assets/images/emoji/urn.pngbin742 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone1.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone2.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone3.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone4.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/v_tone5.pngbin1009 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vertical_traffic_light.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vhs.pngbin632 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vibration_mode.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/video_camera.pngbin1611 -> 0 bytes
-rw-r--r--app/assets/images/emoji/video_game.pngbin765 -> 0 bytes
-rw-r--r--app/assets/images/emoji/violin.pngbin1156 -> 0 bytes
-rw-r--r--app/assets/images/emoji/virgo.pngbin618 -> 0 bytes
-rw-r--r--app/assets/images/emoji/volcano.pngbin1257 -> 0 bytes
-rw-r--r--app/assets/images/emoji/volleyball.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vs.pngbin604 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone1.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone2.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone3.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone4.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/vulcan_tone5.pngbin1083 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking.pngbin1082 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone1.pngbin1084 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone2.pngbin1084 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone3.pngbin1066 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone4.pngbin1075 -> 0 bytes
-rw-r--r--app/assets/images/emoji/walking_tone5.pngbin1065 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waning_crescent_moon.pngbin1213 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waning_gibbous_moon.pngbin1208 -> 0 bytes
-rw-r--r--app/assets/images/emoji/warning.pngbin565 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wastebasket.pngbin2414 -> 0 bytes
-rw-r--r--app/assets/images/emoji/watch.pngbin785 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_buffalo.pngbin1536 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo.pngbin1755 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone1.pngbin1758 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone2.pngbin1756 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone3.pngbin1760 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone4.pngbin1749 -> 0 bytes
-rw-r--r--app/assets/images/emoji/water_polo_tone5.pngbin1748 -> 0 bytes
-rw-r--r--app/assets/images/emoji/watermelon.pngbin1275 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone1.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone2.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone3.pngbin1295 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone4.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wave_tone5.pngbin1300 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wavy_dash.pngbin359 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waxing_crescent_moon.pngbin1199 -> 0 bytes
-rw-r--r--app/assets/images/emoji/waxing_gibbous_moon.pngbin1229 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wc.pngbin752 -> 0 bytes
-rw-r--r--app/assets/images/emoji/weary.pngbin871 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wedding.pngbin1260 -> 0 bytes
-rw-r--r--app/assets/images/emoji/whale.pngbin1572 -> 0 bytes
-rw-r--r--app/assets/images/emoji/whale2.pngbin1196 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wheel_of_dharma.pngbin666 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wheelchair.pngbin683 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_check_mark.pngbin547 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_circle.pngbin351 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_flower.pngbin941 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_large_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_medium_small_square.pngbin110 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_medium_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_small_square.pngbin108 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_square_button.pngbin122 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_cloud.pngbin968 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_rain_cloud.pngbin1161 -> 0 bytes
-rw-r--r--app/assets/images/emoji/white_sun_small_cloud.pngbin989 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wilted_rose.pngbin1349 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wind_blowing_face.pngbin1827 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wind_chime.pngbin1046 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wine_glass.pngbin655 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wink.pngbin746 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wolf.pngbin1528 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone1.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone2.pngbin1212 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone3.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone4.pngbin1195 -> 0 bytes
-rw-r--r--app/assets/images/emoji/woman_tone5.pngbin1202 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womans_clothes.pngbin1042 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womans_hat.pngbin1553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/womens.pngbin577 -> 0 bytes
-rw-r--r--app/assets/images/emoji/worried.pngbin715 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrench.pngbin418 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers.pngbin2556 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone1.pngbin2563 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone2.pngbin2553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone3.pngbin2541 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone4.pngbin2553 -> 0 bytes
-rw-r--r--app/assets/images/emoji/wrestlers_tone5.pngbin2542 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand.pngbin1001 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone1.pngbin988 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone2.pngbin987 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone3.pngbin977 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone4.pngbin973 -> 0 bytes
-rw-r--r--app/assets/images/emoji/writing_hand_tone5.pngbin970 -> 0 bytes
-rw-r--r--app/assets/images/emoji/x.pngbin298 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yellow_heart.pngbin435 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yen.pngbin421 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yin_yang.pngbin776 -> 0 bytes
-rw-r--r--app/assets/images/emoji/yum.pngbin896 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zap.pngbin413 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zero.pngbin560 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zipper_mouth.pngbin722 -> 0 bytes
-rw-r--r--app/assets/images/emoji/zzz.pngbin540 -> 0 bytes
-rw-r--r--app/assets/javascripts/api.js24
-rw-r--r--app/assets/javascripts/awards_handler.js18
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue4
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js118
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js443
-rw-r--r--app/assets/javascripts/behaviors/markdown/editor_extensions.js106
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/bold.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/code.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_diff.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_html.js46
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/italic.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/link.js21
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/math.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/strike.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/blockquote.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/code_block.js99
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_details.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_term.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/details.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/doc.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/emoji.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/hard_break.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/heading.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/image.js52
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/list_item.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/paragraph.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/reference.js52
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/summary.js27
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table.js25
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_body.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_cell.js35
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_head.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js43
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js33
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_row.js38
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js49
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/text.js20
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/video.js54
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js19
-rw-r--r--app/assets/javascripts/behaviors/markdown/schema.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/serializer.js24
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js47
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js9
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js20
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue11
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue4
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js1
-rw-r--r--app/assets/javascripts/boards/services/board_service.js3
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js65
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue192
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue22
-rw-r--r--app/assets/javascripts/clusters/constants.js9
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js22
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/contextual_sidebar.js66
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue72
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue33
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue83
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue26
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue66
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue26
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue52
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue7
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue4
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue1
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue9
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue1
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue10
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue92
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue165
-rw-r--r--app/assets/javascripts/diffs/constants.js10
-rw-r--r--app/assets/javascripts/diffs/index.js64
-rw-r--r--app/assets/javascripts/diffs/mixins/draft_comments.js7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js56
-rw-r--r--app/assets/javascripts/diffs/store/getters.js30
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js5
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js19
-rw-r--r--app/assets/javascripts/diffs/store/utils.js85
-rw-r--r--app/assets/javascripts/diffs/workers/tree_worker.js14
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js7
-rw-r--r--app/assets/javascripts/dismissable_callout.js27
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/emoji/index.js72
-rw-r--r--app/assets/javascripts/emoji/no_emoji_validator.js63
-rw-r--r--app/assets/javascripts/emoji/support/index.js3
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue108
-rw-r--r--app/assets/javascripts/environments/components/container.vue10
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue43
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue14
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue48
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js8
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue5
-rw-r--r--app/assets/javascripts/environments/index.js2
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js40
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js4
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js3
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue124
-rw-r--r--app/assets/javascripts/error_tracking/index.js31
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js7
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js52
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js19
-rw-r--r--app/assets/javascripts/error_tracking/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/mutations.js14
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue129
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue91
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue82
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js27
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js91
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js44
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/index.js16
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js61
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/state.js12
-rw-r--r--app/assets/javascripts/error_tracking_settings/utils.js18
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js4
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js16
-rw-r--r--app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js8
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js133
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js68
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js83
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js103
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js20
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js141
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js22
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js125
-rw-r--r--app/assets/javascripts/fly_out_nav.js5
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue10
-rw-r--r--app/assets/javascripts/frequent_items/index.js6
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js61
-rw-r--r--app/assets/javascripts/gl_dropdown.js18
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue2
-rw-r--r--app/assets/javascripts/groups_select.js172
-rw-r--r--app/assets/javascripts/header.js6
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue2
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue227
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue104
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue46
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue15
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue12
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue72
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue4
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue4
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js21
-rw-r--r--app/assets/javascripts/ide/ide_router.js2
-rw-r--r--app/assets/javascripts/ide/index.js20
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js6
-rw-r--r--app/assets/javascripts/ide/lib/files.js113
-rw-r--r--app/assets/javascripts/ide/services/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions.js70
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js32
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js29
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js46
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js16
-rw-r--r--app/assets/javascripts/ide/stores/utils.js5
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js100
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue101
-rw-r--r--app/assets/javascripts/import_projects/components/import_status.vue47
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue55
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue110
-rw-r--r--app/assets/javascripts/import_projects/constants.js48
-rw-r--r--app/assets/javascripts/import_projects/event_hub.js (renamed from app/assets/javascripts/monitoring/event_hub.js)0
-rw-r--r--app/assets/javascripts/import_projects/index.js48
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js106
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js20
-rw-r--r--app/assets/javascripts/import_projects/store/index.js16
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js11
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js55
-rw-r--r--app/assets/javascripts/import_projects/store/state.js15
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js12
-rw-r--r--app/assets/javascripts/issuable_context.js12
-rw-r--r--app/assets/javascripts/issuable_form.js63
-rw-r--r--app/assets/javascripts/issuable_suggestions/index.js4
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue62
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue24
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue6
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js11
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue21
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue70
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue6
-rw-r--r--app/assets/javascripts/jobs/store/getters.js27
-rw-r--r--app/assets/javascripts/label_manager.js13
-rw-r--r--app/assets/javascripts/labels_select.js32
-rw-r--r--app/assets/javascripts/layout_nav.js65
-rw-r--r--app/assets/javascripts/lazy_loader.js1
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/chart_utils.js83
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js87
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js164
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js3
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js40
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/icon_utils.js18
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js19
-rw-r--r--app/assets/javascripts/lib/utils/poll.js20
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js162
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js23
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js55
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js10
-rw-r--r--app/assets/javascripts/locale/sprintf.js4
-rw-r--r--app/assets/javascripts/main.js128
-rw-r--r--app/assets/javascripts/member_expiration_date.js1
-rw-r--r--app/assets/javascripts/members.js27
-rw-r--r--app/assets/javascripts/merge_request.js9
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue234
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue94
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue329
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue118
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue151
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue62
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue65
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_info.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue33
-rw-r--r--app/assets/javascripts/monitoring/constants.js10
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js86
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js14
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js42
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js44
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js223
-rw-r--r--app/assets/javascripts/mr_notes/index.js72
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js70
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue15
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue3
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue15
-rw-r--r--app/assets/javascripts/notebook/cells/output/image.vue20
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue104
-rw-r--r--app/assets/javascripts/notebook/cells/prompt.vue10
-rw-r--r--app/assets/javascripts/notebook/index.vue4
-rw-r--r--app/assets/javascripts/notes.js22
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue33
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue25
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue67
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue52
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue28
-rw-r--r--app/assets/javascripts/notes/components/discussion_reply_placeholder.vue17
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue28
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue34
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue31
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue30
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue12
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue28
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue14
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue160
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue50
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue44
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue8
-rw-r--r--app/assets/javascripts/notes/constants.js7
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js10
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js73
-rw-r--r--app/assets/javascripts/notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js23
-rw-r--r--app/assets/javascripts/notes/stores/utils.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/show/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/explore/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/edit/index.js (renamed from app/assets/javascripts/pages/groups/clusters/update/index.js)0
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/index.js9
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js3
-rw-r--r--app/assets/javascripts/pages/import/gitea/status/index.js7
-rw-r--r--app/assets/javascripts/pages/import/github/status/index.js7
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js60
-rw-r--r--app/assets/javascripts/pages/projects/clusters/edit/index.js (renamed from app/assets/javascripts/pages/projects/clusters/update/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/environments/metrics/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js91
-rw-r--r--app/assets/javascripts/pages/projects/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/issues/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue8
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js81
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue4
-rw-r--r--app/assets/javascripts/pages/projects/tags/releases/index.js8
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js7
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js5
-rw-r--r--app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js32
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js23
-rw-r--r--app/assets/javascripts/pages/users/user_overview_block.js18
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue7
-rw-r--r--app/assets/javascripts/persistent_user_callout.js42
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue45
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue36
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_component_mixin.js44
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js9
-rw-r--r--app/assets/javascripts/project_select.js174
-rw-r--r--app/assets/javascripts/project_select_combo_button.js10
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue4
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/project_new.js54
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue8
-rw-r--r--app/assets/javascripts/releases/components/app.vue6
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue6
-rw-r--r--app/assets/javascripts/releases/store/actions.js4
-rw-r--r--app/assets/javascripts/reports/components/modal_open_name.vue2
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue2
-rw-r--r--app/assets/javascripts/serverless/components/environment_row.vue65
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue56
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue48
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue56
-rw-r--r--app/assets/javascripts/serverless/components/pod_box.vue36
-rw-r--r--app/assets/javascripts/serverless/components/url.vue38
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js67
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_details_store.js11
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_store.js11
-rw-r--r--app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue30
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue4
-rw-r--r--app/assets/javascripts/task_list.js72
-rw-r--r--app/assets/javascripts/terminal/terminal.js3
-rw-r--r--app/assets/javascripts/users_select.js217
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue91
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue70
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue291
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/mr_widget_pipeline.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/empty_component.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue299
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue126
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row_header.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue116
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/panel_resizer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/default.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue8
-rw-r--r--app/assets/javascripts/vue_shared/mixins/is_ee.js10
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js155
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/actions.js20
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/index.js10
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js4
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/mutations.js18
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/state.js4
-rw-r--r--app/assets/stylesheets/application.scss10
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss20
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss380
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/animations.scss74
-rw-r--r--app/assets/stylesheets/framework/asciidoctor.scss2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss88
-rw-r--r--app/assets/stylesheets/framework/awards.scss26
-rw-r--r--app/assets/stylesheets/framework/blocks.scss6
-rw-r--r--app/assets/stylesheets/framework/buttons.scss11
-rw-r--r--app/assets/stylesheets/framework/common.scss81
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss133
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss6
-rw-r--r--app/assets/stylesheets/framework/emojis.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss20
-rw-r--r--app/assets/stylesheets/framework/filters.scss11
-rw-r--r--app/assets/stylesheets/framework/forms.scss16
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss40
-rw-r--r--app/assets/stylesheets/framework/header.scss35
-rw-r--r--app/assets/stylesheets/framework/icons.scss4
-rw-r--r--app/assets/stylesheets/framework/images.scss3
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss5
-rw-r--r--app/assets/stylesheets/framework/layout.scss8
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/logo.scss35
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss20
-rw-r--r--app/assets/stylesheets/framework/mixins.scss32
-rw-r--r--app/assets/stylesheets/framework/mobile.scss88
-rw-r--r--app/assets/stylesheets/framework/modal.scss43
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/selects.scss20
-rw-r--r--app/assets/stylesheets/framework/spinner.scss51
-rw-r--r--app/assets/stylesheets/framework/stacked_progress_bar.scss8
-rw-r--r--app/assets/stylesheets/framework/system_messages.scss110
-rw-r--r--app/assets/stylesheets/framework/tables.scss9
-rw-r--r--app/assets/stylesheets/framework/terms.scss1
-rw-r--r--app/assets/stylesheets/framework/toggle.scss14
-rw-r--r--app/assets/stylesheets/framework/typography.scss37
-rw-r--r--app/assets/stylesheets/framework/variables.scss84
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss15
-rw-r--r--app/assets/stylesheets/highlight/common.scss18
-rw-r--r--app/assets/stylesheets/highlight/dark.scss259
-rw-r--r--app/assets/stylesheets/highlight/embedded.scss2
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss257
-rw-r--r--app/assets/stylesheets/highlight/none.scss242
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss284
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss292
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss261
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss259
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss238
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss286
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss294
-rw-r--r--app/assets/stylesheets/highlight/themes/white.scss3
-rw-r--r--app/assets/stylesheets/highlight/white.scss3
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss80
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss67
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss72
-rw-r--r--app/assets/stylesheets/page_bundles/xterm.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss47
-rw-r--r--app/assets/stylesheets/pages/branches.scss13
-rw-r--r--app/assets/stylesheets/pages/builds.scss30
-rw-r--r--app/assets/stylesheets/pages/clusters.scss14
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss1
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss190
-rw-r--r--app/assets/stylesheets/pages/editor.scss7
-rw-r--r--app/assets/stylesheets/pages/environments.scss24
-rw-r--r--app/assets/stylesheets/pages/graph.scss8
-rw-r--r--app/assets/stylesheets/pages/groups.scss110
-rw-r--r--app/assets/stylesheets/pages/help.scss2
-rw-r--r--app/assets/stylesheets/pages/import.scss51
-rw-r--r--app/assets/stylesheets/pages/issuable.scss19
-rw-r--r--app/assets/stylesheets/pages/issues.scss24
-rw-r--r--app/assets/stylesheets/pages/issues/issue_count_badge.scss6
-rw-r--r--app/assets/stylesheets/pages/labels.scss16
-rw-r--r--app/assets/stylesheets/pages/login.scss3
-rw-r--r--app/assets/stylesheets/pages/members.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss96
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss90
-rw-r--r--app/assets/stylesheets/pages/milestone.scss9
-rw-r--r--app/assets/stylesheets/pages/note_form.scss18
-rw-r--r--app/assets/stylesheets/pages/notes.scss76
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss118
-rw-r--r--app/assets/stylesheets/pages/profile.scss12
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss201
-rw-r--r--app/assets/stylesheets/pages/reports.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/assets/stylesheets/pages/serverless.scss3
-rw-r--r--app/assets/stylesheets/pages/settings.scss8
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss18
-rw-r--r--app/assets/stylesheets/pages/tree.scss20
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/controllers/admin/appearances_controller.rb5
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb8
-rw-r--r--app/controllers/admin/users_controller.rb12
-rw-r--r--app/controllers/application_controller.rb35
-rw-r--r--app/controllers/clusters/clusters_controller.rb4
-rw-r--r--app/controllers/concerns/boards_responses.rb10
-rw-r--r--app/controllers/concerns/continue_params.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb6
-rw-r--r--app/controllers/concerns/issuable_collections.rb5
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb60
-rw-r--r--app/controllers/concerns/issues_action.rb33
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb25
-rw-r--r--app/controllers/concerns/milestone_actions.rb2
-rw-r--r--app/controllers/concerns/notes_actions.rb57
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/concerns/record_user_last_activity.rb27
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/send_file_upload.rb19
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb18
-rw-r--r--app/controllers/dashboard/application_controller.rb1
-rw-r--r--app/controllers/dashboard/milestones_controller.rb6
-rw-r--r--app/controllers/dashboard/projects_controller.rb11
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/explore/projects_controller.rb9
-rw-r--r--app/controllers/google_api/authorizations_controller.rb32
-rw-r--r--app/controllers/graphql_controller.rb9
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/children_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb23
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/help_controller.rb7
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb11
-rw-r--r--app/controllers/import/gitea_controller.rb20
-rw-r--r--app/controllers/import/github_controller.rb116
-rw-r--r--app/controllers/notification_settings_controller.rb7
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb21
-rw-r--r--app/controllers/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb11
-rw-r--r--app/controllers/profiles/preferences_controller.rb12
-rw-r--r--app/controllers/profiles_controller.rb1
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb9
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb13
-rw-r--r--app/controllers/projects/blob_controller.rb4
-rw-r--r--app/controllers/projects/branches_controller.rb3
-rw-r--r--app/controllers/projects/build_artifacts_controller.rb9
-rw-r--r--app/controllers/projects/ci/lints_controller.rb8
-rw-r--r--app/controllers/projects/commit_controller.rb10
-rw-r--r--app/controllers/projects/discussions_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb40
-rw-r--r--app/controllers/projects/error_tracking_controller.rb84
-rw-r--r--app/controllers/projects/git_http_controller.rb13
-rw-r--r--app/controllers/projects/graphs_controller.rb9
-rw-r--r--app/controllers/projects/group_links_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb33
-rw-r--r--app/controllers/projects/jobs_controller.rb14
-rw-r--r--app/controllers/projects/lfs_locks_api_controller.rb10
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb19
-rw-r--r--app/controllers/projects/milestones_controller.rb4
-rw-r--r--app/controllers/projects/pages_controller.rb5
-rw-r--r--app/controllers/projects/pages_domains_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb11
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb31
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/settings/operations_controller.rb74
-rw-r--r--app/controllers/projects/snippets_controller.rb9
-rw-r--r--app/controllers/projects/tags_controller.rb15
-rw-r--r--app/controllers/projects/tree_controller.rb14
-rw-r--r--app/controllers/projects/triggers_controller.rb7
-rw-r--r--app/controllers/projects_controller.rb18
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb4
-rw-r--r--app/controllers/snippets_controller.rb8
-rw-r--r--app/controllers/uploads_controller.rb10
-rw-r--r--app/finders/admin/runners_finder.rb11
-rw-r--r--app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb44
-rw-r--r--app/finders/concerns/finder_methods.rb4
-rw-r--r--app/finders/contributed_projects_finder.rb7
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb72
-rw-r--r--app/finders/issues_finder.rb12
-rw-r--r--app/finders/merge_requests_finder.rb10
-rw-r--r--app/finders/milestones_finder.rb21
-rw-r--r--app/finders/projects/daily_statistics_finder.rb21
-rw-r--r--app/finders/projects/serverless/functions_finder.rb31
-rw-r--r--app/finders/releases_finder.rb14
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/graphql/mutations/merge_requests/base.rb3
-rw-r--r--app/graphql/resolvers/base_resolver.rb7
-rw-r--r--app/graphql/resolvers/issues_resolver.rb32
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb25
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb36
-rw-r--r--app/graphql/resolvers/metadata_resolver.rb11
-rw-r--r--app/graphql/types/issuable_state_enum.rb12
-rw-r--r--app/graphql/types/issue_state_enum.rb8
-rw-r--r--app/graphql/types/issue_type.rb12
-rw-r--r--app/graphql/types/merge_request_state_enum.rb10
-rw-r--r--app/graphql/types/merge_request_type.rb10
-rw-r--r--app/graphql/types/metadata_type.rb10
-rw-r--r--app/graphql/types/permission_types/project.rb2
-rw-r--r--app/graphql/types/project_type.rb18
-rw-r--r--app/graphql/types/query_type.rb13
-rw-r--r--app/helpers/appearances_helper.rb38
-rw-r--r--app/helpers/application_helper.rb28
-rw-r--r--app/helpers/application_settings_helper.rb8
-rw-r--r--app/helpers/auth_helper.rb9
-rw-r--r--app/helpers/auto_devops_helper.rb37
-rw-r--r--app/helpers/blob_helper.rb38
-rw-r--r--app/helpers/ci_status_helper.rb17
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/count_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb24
-rw-r--r--app/helpers/emails_helper.rb48
-rw-r--r--app/helpers/environments_helper.rb8
-rw-r--r--app/helpers/external_wiki_helper.rb12
-rw-r--r--app/helpers/import_helper.rb40
-rw-r--r--app/helpers/issuables_helper.rb8
-rw-r--r--app/helpers/labels_helper.rb4
-rw-r--r--app/helpers/markup_helper.rb8
-rw-r--r--app/helpers/members_helper.rb17
-rw-r--r--app/helpers/merge_requests_helper.rb6
-rw-r--r--app/helpers/milestones_helper.rb15
-rw-r--r--app/helpers/namespaces_helper.rb6
-rw-r--r--app/helpers/notes_helper.rb5
-rw-r--r--app/helpers/preferences_helper.rb20
-rw-r--r--app/helpers/profiles_helper.rb4
-rw-r--r--app/helpers/projects/error_tracking_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb59
-rw-r--r--app/helpers/runners_helper.rb10
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/snippets_helper.rb10
-rw-r--r--app/helpers/sorting_helper.rb22
-rw-r--r--app/helpers/user_callouts_helper.rb7
-rw-r--r--app/helpers/users_helper.rb9
-rw-r--r--app/helpers/workhorse_helper.rb5
-rw-r--r--app/mailers/abuse_report_mailer.rb4
-rw-r--r--app/mailers/email_rejection_mailer.rb4
-rw-r--r--app/mailers/emails/issues.rb12
-rw-r--r--app/mailers/emails/pipelines.rb2
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/mailers/repository_check_mailer.rb4
-rw-r--r--app/models/active_session.rb6
-rw-r--r--app/models/appearance.rb48
-rw-r--r--app/models/application_record.rb18
-rw-r--r--app/models/application_setting.rb14
-rw-r--r--app/models/blob.rb10
-rw-r--r--app/models/blob_viewer/base.rb6
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb8
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/board_group_recent_visit.rb9
-rw-r--r--app/models/board_project_recent_visit.rb9
-rw-r--r--app/models/ci/bridge.rb29
-rw-r--r--app/models/ci/build.rb264
-rw-r--r--app/models/ci/build_metadata.rb14
-rw-r--r--app/models/ci/build_trace_chunk.rb4
-rw-r--r--app/models/ci/group_variable.rb1
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb109
-rw-r--r--app/models/ci/pipeline_chat_data.rb13
-rw-r--r--app/models/ci/pipeline_enums.rb3
-rw-r--r--app/models/ci/runner.rb7
-rw-r--r--app/models/ci/stage.rb1
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/ci/variable.rb1
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb4
-rw-r--r--app/models/clusters/applications/knative.rb42
-rw-r--r--app/models/clusters/applications/prometheus.rb35
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb44
-rw-r--r--app/models/clusters/concerns/application_status.rb29
-rw-r--r--app/models/clusters/concerns/application_version.rb8
-rw-r--r--app/models/clusters/platforms/kubernetes.rb11
-rw-r--r--app/models/commit.rb11
-rw-r--r--app/models/commit_collection.rb13
-rw-r--r--app/models/commit_range.rb8
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/blob_language_from_git_attributes.rb2
-rw-r--r--app/models/concerns/blob_like.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb44
-rw-r--r--app/models/concerns/cacheable_attributes.rb7
-rw-r--r--app/models/concerns/ci/contextable.rb108
-rw-r--r--app/models/concerns/ci/metadatable.rb69
-rw-r--r--app/models/concerns/ci/processable.rb31
-rw-r--r--app/models/concerns/closed_at_filterable.rb14
-rw-r--r--app/models/concerns/discussion_on_diff.rb20
-rw-r--r--app/models/concerns/fast_destroy_all.rb2
-rw-r--r--app/models/concerns/feature_gate.rb2
-rw-r--r--app/models/concerns/has_ref.rb28
-rw-r--r--app/models/concerns/has_status.rb10
-rw-r--r--app/models/concerns/has_variable.rb6
-rw-r--r--app/models/concerns/iid_routes.rb2
-rw-r--r--app/models/concerns/issuable.rb63
-rw-r--r--app/models/concerns/manual_inverse_association.rb4
-rw-r--r--app/models/concerns/maskable.rb22
-rw-r--r--app/models/concerns/milestoneish.rb25
-rw-r--r--app/models/concerns/mirror_authentication.rb2
-rw-r--r--app/models/concerns/noteable.rb19
-rw-r--r--app/models/concerns/reactive_caching.rb4
-rw-r--r--app/models/concerns/sha_attribute.rb10
-rw-r--r--app/models/concerns/taskable.rb8
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb16
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb52
-rw-r--r--app/models/concerns/with_uploads.rb34
-rw-r--r--app/models/container_repository.rb11
-rw-r--r--app/models/dashboard_group_milestone.rb27
-rw-r--r--app/models/dashboard_milestone.rb10
-rw-r--r--app/models/diff_note.rb8
-rw-r--r--app/models/diff_viewer/base.rb39
-rw-r--r--app/models/diff_viewer/image.rb2
-rw-r--r--app/models/diff_viewer/rich.rb2
-rw-r--r--app/models/diff_viewer/server_side.rb12
-rw-r--r--app/models/diff_viewer/simple.rb2
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/email.rb2
-rw-r--r--app/models/environment.rb20
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb157
-rw-r--r--app/models/external_issue.rb8
-rw-r--r--app/models/global_milestone.rb117
-rw-r--r--app/models/gpg_signature.rb7
-rw-r--r--app/models/group.rb8
-rw-r--r--app/models/group_milestone.rb30
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/individual_note_discussion.rb12
-rw-r--r--app/models/internal_id.rb17
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/label.rb1
-rw-r--r--app/models/label_note.rb2
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/lfs_download_object.rb22
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb5
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb178
-rw-r--r--app/models/merge_request_diff.rb121
-rw-r--r--app/models/merge_request_diff_file.rb14
-rw-r--r--app/models/milestone.rb67
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/network/graph.rb9
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/note_diff_file.rb15
-rw-r--r--app/models/notification_recipient.rb6
-rw-r--r--app/models/personal_access_token.rb5
-rw-r--r--app/models/pool_repository.rb10
-rw-r--r--app/models/programming_language.rb6
-rw-r--r--app/models/project.rb312
-rw-r--r--app/models/project_auto_devops.rb6
-rw-r--r--app/models/project_daily_statistic.rb10
-rw-r--r--app/models/project_feature.rb34
-rw-r--r--app/models/project_import_data.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb32
-rw-r--r--app/models/project_services/campfire_service.rb2
-rw-r--r--app/models/project_services/deployment_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb311
-rw-r--r--app/models/project_services/irker_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/models/project_services/kubernetes_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb4
-rw-r--r--app/models/project_services/teamcity_service.rb6
-rw-r--r--app/models/project_services/youtrack_service.rb40
-rw-r--r--app/models/project_team.rb12
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/prometheus_metric.rb84
-rw-r--r--app/models/protected_branch.rb14
-rw-r--r--app/models/release.rb42
-rw-r--r--app/models/releases/link.rb22
-rw-r--r--app/models/releases/source.rb35
-rw-r--r--app/models/remote_mirror.rb14
-rw-r--r--app/models/repository.rb56
-rw-r--r--app/models/sent_notification.rb25
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/snippet.rb10
-rw-r--r--app/models/ssh_host_key.rb8
-rw-r--r--app/models/storage/hashed_project.rb2
-rw-r--r--app/models/storage/legacy_project.rb11
-rw-r--r--app/models/suggestion.rb7
-rw-r--r--app/models/todo.rb5
-rw-r--r--app/models/user.rb28
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/board_policy.rb16
-rw-r--r--app/policies/ci/pipeline_policy.rb9
-rw-r--r--app/policies/concerns/clusterable_actions.rb14
-rw-r--r--app/policies/container_repository_policy.rb5
-rw-r--r--app/policies/global_policy.rb4
-rw-r--r--app/policies/group_policy.rb12
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/issue_policy.rb1
-rw-r--r--app/policies/note_policy.rb1
-rw-r--r--app/policies/personal_snippet_policy.rb7
-rw-r--r--app/policies/project_policy.rb49
-rw-r--r--app/policies/project_snippet_policy.rb13
-rw-r--r--app/policies/release_policy.rb5
-rw-r--r--app/presenters/ci/build_runner_presenter.rb42
-rw-r--r--app/presenters/ci/trigger_presenter.rb19
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/commit_presenter.rb13
-rw-r--r--app/presenters/merge_request_presenter.rb14
-rw-r--r--app/presenters/project_presenter.rb23
-rw-r--r--app/serializers/acts_as_taggable_on/tag_entity.rb6
-rw-r--r--app/serializers/acts_as_taggable_on/tag_serializer.rb5
-rw-r--r--app/serializers/base_serializer.rb10
-rw-r--r--app/serializers/cluster_application_entity.rb2
-rw-r--r--app/serializers/concerns/user_status_tooltip.rb2
-rw-r--r--app/serializers/container_repository_entity.rb2
-rw-r--r--app/serializers/container_tag_entity.rb2
-rw-r--r--app/serializers/deployment_entity.rb10
-rw-r--r--app/serializers/diff_file_base_entity.rb21
-rw-r--r--app/serializers/diff_file_entity.rb8
-rw-r--r--app/serializers/diff_viewer_entity.rb5
-rw-r--r--app/serializers/environment_entity.rb1
-rw-r--r--app/serializers/error_tracking/error_entity.rb10
-rw-r--r--app/serializers/error_tracking/error_serializer.rb7
-rw-r--r--app/serializers/error_tracking/project_entity.rb7
-rw-r--r--app/serializers/error_tracking/project_serializer.rb7
-rw-r--r--app/serializers/merge_request_basic_entity.rb1
-rw-r--r--app/serializers/merge_request_diff_entity.rb8
-rw-r--r--app/serializers/merge_request_for_pipeline_entity.rb17
-rw-r--r--app/serializers/merge_request_widget_commit_entity.rb7
-rw-r--r--app/serializers/merge_request_widget_entity.rb29
-rw-r--r--app/serializers/namespace_basic_entity.rb6
-rw-r--r--app/serializers/namespace_serializer.rb5
-rw-r--r--app/serializers/pipeline_entity.rb16
-rw-r--r--app/serializers/pipeline_serializer.rb1
-rw-r--r--app/serializers/project_import_entity.rb13
-rw-r--r--app/serializers/project_serializer.rb12
-rw-r--r--app/serializers/projects/serverless/service_entity.rb36
-rw-r--r--app/serializers/provider_repo_entity.rb25
-rw-r--r--app/serializers/provider_repo_serializer.rb5
-rw-r--r--app/serializers/tree_entity.rb15
-rw-r--r--app/serializers/tree_root_entity.rb27
-rw-r--r--app/serializers/tree_serializer.rb5
-rw-r--r--app/services/application_settings/update_service.rb2
-rw-r--r--app/services/applications/create_service.rb8
-rw-r--r--app/services/auth/container_registry_authentication_service.rb3
-rw-r--r--app/services/boards/issues/move_service.rb2
-rw-r--r--app/services/boards/lists/destroy_service.rb2
-rw-r--r--app/services/boards/visits/latest_service.rb14
-rw-r--r--app/services/ci/create_pipeline_service.rb25
-rw-r--r--app/services/ci/destroy_expired_job_artifacts_service.rb38
-rw-r--r--app/services/ci/destroy_pipeline_service.rb2
-rw-r--r--app/services/ci/pipeline_trigger_service.rb10
-rw-r--r--app/services/ci/process_pipeline_service.rb16
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/clusters/applications/base_helm_service.rb4
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb23
-rw-r--r--app/services/clusters/applications/create_service.rb6
-rw-r--r--app/services/clusters/applications/schedule_installation_service.rb19
-rw-r--r--app/services/clusters/applications/upgrade_service.rb28
-rw-r--r--app/services/commits/create_service.rb13
-rw-r--r--app/services/commits/tag_service.rb3
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb15
-rw-r--r--app/services/concerns/users/participable_service.rb15
-rw-r--r--app/services/create_branch_service.rb4
-rw-r--r--app/services/create_release_service.rb35
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/emails/base_service.rb5
-rw-r--r--app/services/emails/create_service.rb9
-rw-r--r--app/services/error_tracking/list_issues_service.rb53
-rw-r--r--app/services/error_tracking/list_projects_service.rb44
-rw-r--r--app/services/files/multi_service.rb3
-rw-r--r--app/services/git_push_service.rb11
-rw-r--r--app/services/git_tag_push_service.rb9
-rw-r--r--app/services/groups/create_service.rb6
-rw-r--r--app/services/groups/nested_create_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb10
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/import/base_service.rb35
-rw-r--r--app/services/import/github_service.rb48
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/issuable_base_service.rb71
-rw-r--r--app/services/issues/build_service.rb12
-rw-r--r--app/services/issues/import_csv_service.rb53
-rw-r--r--app/services/issues/update_service.rb11
-rw-r--r--app/services/labels/create_service.rb2
-rw-r--r--app/services/labels/promote_service.rb2
-rw-r--r--app/services/labels/update_service.rb3
-rw-r--r--app/services/lfs/locks_finder_service.rb2
-rw-r--r--app/services/members/base_service.rb6
-rw-r--r--app/services/members/create_service.rb14
-rw-r--r--app/services/members/destroy_service.rb32
-rw-r--r--app/services/members/update_service.rb9
-rw-r--r--app/services/merge_requests/base_service.rb3
-rw-r--r--app/services/merge_requests/build_service.rb25
-rw-r--r--app/services/merge_requests/create_service.rb16
-rw-r--r--app/services/merge_requests/merge_base_service.rb63
-rw-r--r--app/services/merge_requests/merge_service.rb46
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb76
-rw-r--r--app/services/merge_requests/squash_service.rb33
-rw-r--r--app/services/merge_requests/update_service.rb13
-rw-r--r--app/services/milestones/promote_service.rb4
-rw-r--r--app/services/notes/build_service.rb17
-rw-r--r--app/services/notes/create_service.rb15
-rw-r--r--app/services/notes/quick_actions_service.rb33
-rw-r--r--app/services/notification_recipient_service.rb7
-rw-r--r--app/services/notification_service.rb5
-rw-r--r--app/services/preview_markdown_service.rb11
-rw-r--r--app/services/projects/after_rename_service.rb31
-rw-r--r--app/services/projects/autocomplete_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb94
-rw-r--r--app/services/projects/create_from_template_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb24
-rw-r--r--app/services/projects/download_service.rb2
-rw-r--r--app/services/projects/fetch_statistics_increment_service.rb32
-rw-r--r--app/services/projects/fork_service.rb12
-rw-r--r--app/services/projects/group_links/create_service.rb10
-rw-r--r--app/services/projects/hashed_storage/base_attachment_service.rb51
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb67
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb45
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb50
-rw-r--r--app/services/projects/hashed_storage/migration_service.rb39
-rw-r--r--app/services/projects/hashed_storage/rollback_attachments_service.rb34
-rw-r--r--app/services/projects/hashed_storage/rollback_repository_service.rb40
-rw-r--r--app/services/projects/hashed_storage/rollback_service.rb37
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb27
-rw-r--r--app/services/projects/import_error_filter.rb14
-rw-r--r--app/services/projects/import_service.rb22
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb13
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb102
-rw-r--r--app/services/projects/operations/update_service.rb40
-rw-r--r--app/services/projects/protect_default_branch_service.rb67
-rw-r--r--app/services/projects/update_pages_configuration_service.rb34
-rw-r--r--app/services/projects/update_pages_service.rb41
-rw-r--r--app/services/projects/update_service.rb13
-rw-r--r--app/services/prometheus/adapter_service.rb2
-rw-r--r--app/services/protected_branches/api_service.rb21
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb19
-rw-r--r--app/services/push_event_payload_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb46
-rw-r--r--app/services/releases/concerns.rb48
-rw-r--r--app/services/releases/create_service.rb55
-rw-r--r--app/services/releases/destroy_service.rb25
-rw-r--r--app/services/releases/update_service.rb32
-rw-r--r--app/services/search/global_service.rb14
-rw-r--r--app/services/suggestions/apply_service.rb43
-rw-r--r--app/services/suggestions/create_service.rb10
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/tags/create_service.rb7
-rw-r--r--app/services/tags/destroy_service.rb10
-rw-r--r--app/services/task_list_toggle_service.rb72
-rw-r--r--app/services/update_release_service.rb28
-rw-r--r--app/services/upload_service.rb4
-rw-r--r--app/services/users/activity_service.rb7
-rw-r--r--app/services/users/update_service.rb2
-rw-r--r--app/services/validate_new_branch_service.rb4
-rw-r--r--app/uploaders/external_diff_uploader.rb23
-rw-r--r--app/uploaders/file_mover.rb8
-rw-r--r--app/uploaders/personal_file_uploader.rb18
-rw-r--r--app/uploaders/records_uploads.rb20
-rw-r--r--app/validators/sha_validator.rb9
-rw-r--r--app/validators/url_validator.rb10
-rw-r--r--app/views/admin/appearances/_form.html.haml169
-rw-r--r--app/views/admin/appearances/_system_header_footer_form.html.haml33
-rw-r--r--app/views/admin/appearances/show.html.haml7
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml4
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml11
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml1
-rw-r--r--app/views/admin/application_settings/preferences.html.haml11
-rw-r--r--app/views/admin/application_settings/show.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/admin/runners/_runner.html.haml15
-rw-r--r--app/views/admin/runners/index.html.haml23
-rw-r--r--app/views/admin/users/_user.html.haml55
-rw-r--r--app/views/admin/users/_user_detail.html.haml17
-rw-r--r--app/views/admin/users/index.html.haml96
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/ci/status/_icon.html.haml16
-rw-r--r--app/views/ci/variables/_content.html.haml2
-rw-r--r--app/views/clusters/clusters/_buttons.html.haml3
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml2
-rw-r--r--app/views/clusters/clusters/_form.html.haml46
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml6
-rw-r--r--app/views/clusters/clusters/_integration_form.html.haml31
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml50
-rw-r--r--app/views/clusters/clusters/index.html.haml8
-rw-r--r--app/views/clusters/clusters/new.html.haml4
-rw-r--r--app/views/clusters/clusters/show.html.haml16
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml39
-rw-r--r--app/views/clusters/platforms/kubernetes/_form.html.haml58
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml4
-rw-r--r--app/views/dashboard/activity.html.haml3
-rw-r--r--app/views/dashboard/groups/index.html.haml4
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/merge_requests.html.haml2
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml2
-rw-r--r--app/views/dashboard/milestones/index.html.haml2
-rw-r--r--app/views/dashboard/projects/_starred_empty_state.html.haml9
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml11
-rw-r--r--app/views/dashboard/snippets/index.html.haml12
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml2
-rw-r--r--app/views/devise/passwords/edit.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml2
-rw-r--r--app/views/devise/sessions/new.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml6
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/email_rejection_mailer/rejection.html.haml2
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml2
-rw-r--r--app/views/events/_events.html.haml19
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/index.html.haml2
-rw-r--r--app/views/explore/projects/starred.html.haml2
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/explore/snippets/index.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml71
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/labels/index.html.haml7
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml17
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/runners/_group_runners.html.haml2
-rw-r--r--app/views/groups/settings/_general.html.haml2
-rw-r--r--app/views/groups/show.html.haml95
-rw-r--r--app/views/help/_shortcuts.html.haml12
-rw-r--r--app/views/ide/_show.html.haml2
-rw-r--r--app/views/import/_githubish_status.html.haml59
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/import/manifest/status.html.haml2
-rw-r--r--app/views/instance_statistics/conversational_development_index/_callout.html.haml6
-rw-r--r--app/views/instance_statistics/conversational_development_index/_card.html.haml4
-rw-r--r--app/views/instance_statistics/conversational_development_index/_no_data.html.haml6
-rw-r--r--app/views/instance_statistics/conversational_development_index/index.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml30
-rw-r--r--app/views/layouts/_init_client_detection_flags.html.haml7
-rw-r--r--app/views/layouts/_mailer.html.haml6
-rw-r--r--app/views/layouts/application.html.haml7
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/empty_mailer.html.haml5
-rw-r--r--app/views/layouts/empty_mailer.text.erb5
-rw-r--r--app/views/layouts/group.html.haml2
-rw-r--r--app/views/layouts/header/_empty.html.haml4
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml3
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/mailer.text.erb4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml30
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml48
-rw-r--r--app/views/layouts/notify.html.haml2
-rw-r--r--app/views/layouts/notify.text.erb4
-rw-r--r--app/views/layouts/snippets.html.haml2
-rw-r--r--app/views/notify/_note_email.text.erb4
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml6
-rw-r--r--app/views/notify/import_issues_csv_email.html.haml18
-rw-r--r--app/views/notify/import_issues_csv_email.text.erb11
-rw-r--r--app/views/notify/issue_moved_email.html.haml11
-rw-r--r--app/views/notify/issue_moved_email.text.erb4
-rw-r--r--app/views/notify/issue_status_changed_email.html.haml2
-rw-r--r--app/views/notify/issue_status_changed_email.text.erb2
-rw-r--r--app/views/notify/member_access_requested_email.text.erb2
-rw-r--r--app/views/notify/member_invite_accepted_email.text.erb2
-rw-r--r--app/views/notify/member_invited_email.text.erb2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml4
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml4
-rw-r--r--app/views/notify/new_gpg_key_email.html.haml2
-rw-r--r--app/views/notify/new_gpg_key_email.text.erb2
-rw-r--r--app/views/notify/new_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb4
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb4
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.text.erb2
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/notify/new_user_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb6
-rw-r--r--app/views/notify/pipeline_success_email.text.erb6
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml2
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.html.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.text.erb2
-rw-r--r--app/views/notify/reassigned_merge_request_email.html.haml4
-rw-r--r--app/views/notify/reassigned_merge_request_email.text.erb4
-rw-r--r--app/views/notify/resolved_all_discussions_email.html.haml2
-rw-r--r--app/views/notify/resolved_all_discussions_email.text.erb2
-rw-r--r--app/views/profiles/accounts/show.html.haml34
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml6
-rw-r--r--app/views/profiles/passwords/new.html.haml13
-rw-r--r--app/views/profiles/preferences/show.html.haml27
-rw-r--r--app/views/profiles/show.html.haml9
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_export.html.haml7
-rw-r--r--app/views/projects/_home_panel.html.haml43
-rw-r--r--app/views/projects/_issuable_by_email.html.haml9
-rw-r--r--app/views/projects/_md_preview.html.haml16
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml1
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml2
-rw-r--r--app/views/projects/badges/badge_flat-square.svg.erb17
-rw-r--r--app/views/projects/blob/_editor.html.haml4
-rw-r--r--app/views/projects/blob/_markdown_buttons.html.haml13
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml5
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_markup.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml33
-rw-r--r--app/views/projects/branches/_panel.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml25
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml4
-rw-r--r--app/views/projects/buttons/_notifications.html.haml27
-rw-r--r--app/views/projects/ci/builds/_build.html.haml22
-rw-r--r--app/views/projects/ci/lints/_create.html.haml35
-rw-r--r--app/views/projects/ci/lints/show.html.haml12
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml8
-rw-r--r--app/views/projects/commit/_commit_box.html.haml21
-rw-r--r--app/views/projects/commit/_limit_exceeded_message.html.haml4
-rw-r--r--app/views/projects/commit/_other_user_signature_badge.html.haml4
-rw-r--r--app/views/projects/commit/_same_user_different_email_signature_badge.html.haml5
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml4
-rw-r--r--app/views/projects/commit/_unverified_signature_badge.html.haml4
-rw-r--r--app/views/projects/commit/_verified_signature_badge.html.haml5
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml11
-rw-r--r--app/views/projects/commits/_commit.html.haml5
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/deployments/_confirm_rollback_modal.html.haml23
-rw-r--r--app/views/projects/deployments/_rollback.haml3
-rw-r--r--app/views/projects/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/diffs/_render_error.html.haml6
-rw-r--r--app/views/projects/diffs/_warning.html.haml2
-rw-r--r--app/views/projects/edit.html.haml54
-rw-r--r--app/views/projects/environments/folder.html.haml5
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/error_tracking/index.html.haml3
-rw-r--r--app/views/projects/forks/_fork_button.html.haml4
-rw-r--r--app/views/projects/graphs/charts.html.haml12
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_form.html.haml3
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml67
-rw-r--r--app/views/projects/issues/_merge_requests_status.html.haml25
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml41
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml4
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml9
-rw-r--r--app/views/projects/issues/import_csv/_modal.html.haml24
-rw-r--r--app/views/projects/issues/index.html.haml5
-rw-r--r--app/views/projects/issues/show.html.haml15
-rw-r--r--app/views/projects/jobs/index.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml7
-rw-r--r--app/views/projects/merge_requests/_form.html.haml3
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml7
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml3
-rw-r--r--app/views/projects/milestones/_form.html.haml23
-rw-r--r--app/views/projects/milestones/edit.html.haml10
-rw-r--r--app/views/projects/milestones/index.html.haml9
-rw-r--r--app/views/projects/milestones/new.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml28
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/pages/_destroy.haml2
-rw-r--r--app/views/projects/pages/_https_only.html.haml2
-rw-r--r--app/views/projects/pages_domains/_form.html.haml13
-rw-r--r--app/views/projects/pages_domains/edit.html.haml4
-rw-r--r--app/views/projects/pages_domains/new.html.haml8
-rw-r--r--app/views/projects/pages_domains/show.html.haml27
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml37
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml6
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml9
-rw-r--r--app/views/projects/project_members/_groups.html.haml3
-rw-r--r--app/views/projects/project_members/_new_project_group.html.haml5
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml15
-rw-r--r--app/views/projects/project_members/_team.html.haml9
-rw-r--r--app/views/projects/project_members/import.html.haml12
-rw-r--r--app/views/projects/project_members/index.html.haml23
-rw-r--r--app/views/projects/project_templates/_built_in_templates.html.haml6
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/projects/releases/index.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml2
-rw-r--r--app/views/projects/serverless/functions/index.html.haml2
-rw-r--r--app/views/projects/serverless/functions/show.html.haml16
-rw-r--r--app/views/projects/services/prometheus/_metrics.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml19
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml3
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml18
-rw-r--r--app/views/projects/settings/operations/show.html.haml6
-rw-r--r--app/views/projects/snippets/index.html.haml18
-rw-r--r--app/views/projects/snippets/new.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/releases/edit.html.haml3
-rw-r--r--app/views/projects/tags/show.html.haml4
-rw-r--r--app/views/projects/tree/_tree_header.html.haml21
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml16
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml3
-rw-r--r--app/views/projects/wikis/pages.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/search/_category.html.haml26
-rw-r--r--app/views/search/_filter.html.haml20
-rw-r--r--app/views/search/_form.html.haml6
-rw-r--r--app/views/search/_results.html.haml8
-rw-r--r--app/views/search/results/_empty.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml4
-rw-r--r--app/views/search/results/_note.html.haml8
-rw-r--r--app/views/search/results/_snippet_blob.html.haml6
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/search/show.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml13
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml4
-rw-r--r--app/views/shared/_flash_user_callout.html.haml11
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml1
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml1
-rw-r--r--app/views/shared/_personal_access_tokens_created_container.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml4
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml2
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml2
-rw-r--r--app/views/shared/empty_states/_issues.html.haml31
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml19
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_profile_tabs.html.haml19
-rw-r--r--app/views/shared/empty_states/_snippets.html.haml20
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/groups/_list.html.haml14
-rw-r--r--app/views/shared/icons/_express.svg1
-rw-r--r--app/views/shared/icons/_rails.svg1
-rw-r--r--app/views/shared/icons/_spring.svg1
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml3
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml31
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml4
-rw-r--r--app/views/shared/labels/_nav.html.haml4
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml20
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml6
-rw-r--r--app/views/shared/milestones/_milestone.html.haml16
-rw-r--r--app/views/shared/milestones/_search_form.html.haml8
-rw-r--r--app/views/shared/milestones/_tabs.html.haml6
-rw-r--r--app/views/shared/milestones/_top.html.haml11
-rw-r--r--app/views/shared/notes/_comment_button.html.haml19
-rw-r--r--app/views/shared/notes/_edit.html.haml2
-rw-r--r--app/views/shared/notes/_edit_form.html.haml8
-rw-r--r--app/views/shared/notes/_form.html.haml6
-rw-r--r--app/views/shared/notes/_hints.html.haml12
-rw-r--r--app/views/shared/notes/_note.html.haml6
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml14
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/notifications/_new_button.html.haml27
-rw-r--r--app/views/shared/projects/_list.html.haml27
-rw-r--r--app/views/shared/projects/_project.html.haml47
-rw-r--r--app/views/shared/snippets/_form.html.haml3
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_list.html.haml12
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml2
-rw-r--r--app/views/snippets/_snippets.html.haml13
-rw-r--r--app/views/snippets/index.html.haml2
-rw-r--r--app/views/users/_groups.html.haml2
-rw-r--r--app/views/users/_overview.html.haml30
-rw-r--r--app/views/users/show.html.haml6
-rw-r--r--app/workers/all_queues.yml15
-rw-r--r--app/workers/build_finished_worker.rb27
-rw-r--r--app/workers/chat_notification_worker.rb33
-rw-r--r--app/workers/cleanup_container_repository_worker.rb53
-rw-r--r--app/workers/cluster_upgrade_app_worker.rb13
-rw-r--r--app/workers/delete_container_repository_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb14
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb32
-rw-r--r--app/workers/git_garbage_collect_worker.rb1
-rw-r--r--app/workers/hashed_storage/base_worker.rb21
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb16
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb26
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb26
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb16
-rw-r--r--app/workers/import_issues_csv_worker.rb20
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb54
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb2
-rw-r--r--app/workers/object_pool/join_worker.rb11
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/post_receive.rb20
-rw-r--r--app/workers/project_daily_statistics_worker.rb13
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb38
-rw-r--r--app/workers/reactive_caching_worker.rb4
-rw-r--r--app/workers/remote_mirror_notification_worker.rb3
-rw-r--r--app/workers/repository_fork_worker.rb10
-rw-r--r--app/workers/storage_migrator_worker.rb10
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb21
3177 files changed, 22117 insertions, 11495 deletions
diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png
deleted file mode 100644
index 6903ff0304a..00000000000
--- a/app/assets/images/emoji/100.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png
deleted file mode 100644
index 248dc7e55b6..00000000000
--- a/app/assets/images/emoji/1234.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/1F627.png b/app/assets/images/emoji/1F627.png
deleted file mode 100644
index f99026a3bc7..00000000000
--- a/app/assets/images/emoji/1F627.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png
deleted file mode 100644
index 38ca662eded..00000000000
--- a/app/assets/images/emoji/8ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png
deleted file mode 100644
index 8603ff05a17..00000000000
--- a/app/assets/images/emoji/a.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png
deleted file mode 100644
index d9f2d17dea0..00000000000
--- a/app/assets/images/emoji/ab.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png
deleted file mode 100644
index 7688de692a9..00000000000
--- a/app/assets/images/emoji/abc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png
deleted file mode 100644
index 0996a870570..00000000000
--- a/app/assets/images/emoji/abcd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png
deleted file mode 100644
index 8afd7ce99cf..00000000000
--- a/app/assets/images/emoji/accept.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png
deleted file mode 100644
index 3eb4b61bf1d..00000000000
--- a/app/assets/images/emoji/aerial_tramway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png
deleted file mode 100644
index 268d2ac3c8e..00000000000
--- a/app/assets/images/emoji/airplane.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png
deleted file mode 100644
index d66841962f2..00000000000
--- a/app/assets/images/emoji/airplane_arriving.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png
deleted file mode 100644
index a5766f9f4ae..00000000000
--- a/app/assets/images/emoji/airplane_departure.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png
deleted file mode 100644
index b731b15e3a8..00000000000
--- a/app/assets/images/emoji/airplane_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png
deleted file mode 100644
index cdbc2fbb950..00000000000
--- a/app/assets/images/emoji/alarm_clock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png
deleted file mode 100644
index 307a7324249..00000000000
--- a/app/assets/images/emoji/alembic.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png
deleted file mode 100644
index 3b90e97433b..00000000000
--- a/app/assets/images/emoji/alien.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png
deleted file mode 100644
index 6fb8076d766..00000000000
--- a/app/assets/images/emoji/ambulance.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png
deleted file mode 100644
index 96de5056059..00000000000
--- a/app/assets/images/emoji/amphora.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png
deleted file mode 100644
index b036f70a00b..00000000000
--- a/app/assets/images/emoji/anchor.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png
deleted file mode 100644
index 66ea97a3b99..00000000000
--- a/app/assets/images/emoji/angel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png
deleted file mode 100644
index 391694dc07e..00000000000
--- a/app/assets/images/emoji/angel_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png
deleted file mode 100644
index 700cbe6ed2c..00000000000
--- a/app/assets/images/emoji/angel_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png
deleted file mode 100644
index be597437d25..00000000000
--- a/app/assets/images/emoji/angel_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png
deleted file mode 100644
index b06d3c853ef..00000000000
--- a/app/assets/images/emoji/angel_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png
deleted file mode 100644
index 17bd677e334..00000000000
--- a/app/assets/images/emoji/angel_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png
deleted file mode 100644
index d63c2e000e4..00000000000
--- a/app/assets/images/emoji/anger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png
deleted file mode 100644
index f5c97c4d297..00000000000
--- a/app/assets/images/emoji/anger_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png
deleted file mode 100644
index cfc4a6ecde5..00000000000
--- a/app/assets/images/emoji/angry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png
deleted file mode 100644
index 994127ed6b3..00000000000
--- a/app/assets/images/emoji/ant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png
deleted file mode 100644
index da650c60f62..00000000000
--- a/app/assets/images/emoji/apple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png
deleted file mode 100644
index 641a4f68889..00000000000
--- a/app/assets/images/emoji/aquarius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png
deleted file mode 100644
index 21a189d0ede..00000000000
--- a/app/assets/images/emoji/aries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png
deleted file mode 100644
index ee38e3b038e..00000000000
--- a/app/assets/images/emoji/arrow_backward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png
deleted file mode 100644
index 90193bfcb40..00000000000
--- a/app/assets/images/emoji/arrow_double_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png
deleted file mode 100644
index 13543d5eef2..00000000000
--- a/app/assets/images/emoji/arrow_double_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png
deleted file mode 100644
index b8eefd0b19f..00000000000
--- a/app/assets/images/emoji/arrow_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png
deleted file mode 100644
index 5870b9a2241..00000000000
--- a/app/assets/images/emoji/arrow_down_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png
deleted file mode 100644
index 4e2b682857c..00000000000
--- a/app/assets/images/emoji/arrow_forward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png
deleted file mode 100644
index 2d9d24bca80..00000000000
--- a/app/assets/images/emoji/arrow_heading_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png
deleted file mode 100644
index f29bfcfc0de..00000000000
--- a/app/assets/images/emoji/arrow_heading_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png
deleted file mode 100644
index 8c685e0a81b..00000000000
--- a/app/assets/images/emoji/arrow_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png
deleted file mode 100644
index 88b37716078..00000000000
--- a/app/assets/images/emoji/arrow_lower_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png
deleted file mode 100644
index 7e807da7392..00000000000
--- a/app/assets/images/emoji/arrow_lower_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png
deleted file mode 100644
index 4755670b5cc..00000000000
--- a/app/assets/images/emoji/arrow_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png
deleted file mode 100644
index e7258ad3268..00000000000
--- a/app/assets/images/emoji/arrow_right_hook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png
deleted file mode 100644
index af8218a87f7..00000000000
--- a/app/assets/images/emoji/arrow_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png
deleted file mode 100644
index dfa32b97186..00000000000
--- a/app/assets/images/emoji/arrow_up_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png
deleted file mode 100644
index 20a13dcd5cd..00000000000
--- a/app/assets/images/emoji/arrow_up_small.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png
deleted file mode 100644
index f38718fbe34..00000000000
--- a/app/assets/images/emoji/arrow_upper_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png
deleted file mode 100644
index c43e12d0f64..00000000000
--- a/app/assets/images/emoji/arrow_upper_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png
deleted file mode 100644
index 26e49c38388..00000000000
--- a/app/assets/images/emoji/arrows_clockwise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png
deleted file mode 100644
index 8d06d8e0912..00000000000
--- a/app/assets/images/emoji/arrows_counterclockwise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png
deleted file mode 100644
index bd6afe9ff06..00000000000
--- a/app/assets/images/emoji/art.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png
deleted file mode 100644
index c8217317132..00000000000
--- a/app/assets/images/emoji/articulated_lorry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png
deleted file mode 100644
index 2f8e5113803..00000000000
--- a/app/assets/images/emoji/asterisk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png
deleted file mode 100644
index bd0ac55ec8e..00000000000
--- a/app/assets/images/emoji/astonished.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png
deleted file mode 100644
index 423fa07dd5d..00000000000
--- a/app/assets/images/emoji/athletic_shoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png
deleted file mode 100644
index 4d935307b94..00000000000
--- a/app/assets/images/emoji/atm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png
deleted file mode 100644
index 5f4567aa093..00000000000
--- a/app/assets/images/emoji/atom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png
deleted file mode 100644
index 06f0d124aed..00000000000
--- a/app/assets/images/emoji/avocado.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png
deleted file mode 100644
index 25875bc6a14..00000000000
--- a/app/assets/images/emoji/b.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png
deleted file mode 100644
index a4af92c63c7..00000000000
--- a/app/assets/images/emoji/baby.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png
deleted file mode 100644
index 2bd10524180..00000000000
--- a/app/assets/images/emoji/baby_bottle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png
deleted file mode 100644
index dccd96576ea..00000000000
--- a/app/assets/images/emoji/baby_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png
deleted file mode 100644
index 64a10b71710..00000000000
--- a/app/assets/images/emoji/baby_symbol.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png
deleted file mode 100644
index d20911d40db..00000000000
--- a/app/assets/images/emoji/baby_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png
deleted file mode 100644
index b0a9b30ed17..00000000000
--- a/app/assets/images/emoji/baby_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png
deleted file mode 100644
index 7de5286fac1..00000000000
--- a/app/assets/images/emoji/baby_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png
deleted file mode 100644
index 9b7a86ac615..00000000000
--- a/app/assets/images/emoji/baby_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png
deleted file mode 100644
index fe1be34cb88..00000000000
--- a/app/assets/images/emoji/baby_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png
deleted file mode 100644
index d32c5d4f17f..00000000000
--- a/app/assets/images/emoji/back.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png
deleted file mode 100644
index f38a485fbe4..00000000000
--- a/app/assets/images/emoji/bacon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png
deleted file mode 100644
index 7ba15708990..00000000000
--- a/app/assets/images/emoji/badminton.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png
deleted file mode 100644
index 409b593e78a..00000000000
--- a/app/assets/images/emoji/baggage_claim.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png
deleted file mode 100644
index 07916fe6df1..00000000000
--- a/app/assets/images/emoji/balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png
deleted file mode 100644
index 9b6767aea9e..00000000000
--- a/app/assets/images/emoji/ballot_box.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png
deleted file mode 100644
index 284d9573847..00000000000
--- a/app/assets/images/emoji/ballot_box_with_check.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png
deleted file mode 100644
index 5d5e0e728a0..00000000000
--- a/app/assets/images/emoji/bamboo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png
deleted file mode 100644
index f4987279580..00000000000
--- a/app/assets/images/emoji/banana.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png
deleted file mode 100644
index 58a9c528fca..00000000000
--- a/app/assets/images/emoji/bangbang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png
deleted file mode 100644
index dffdcef36a1..00000000000
--- a/app/assets/images/emoji/bank.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png
deleted file mode 100644
index 53c89455008..00000000000
--- a/app/assets/images/emoji/bar_chart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png
deleted file mode 100644
index 896f4d716cf..00000000000
--- a/app/assets/images/emoji/barber.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png
deleted file mode 100644
index f8463f1538b..00000000000
--- a/app/assets/images/emoji/baseball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png
deleted file mode 100644
index 64c76b79c6d..00000000000
--- a/app/assets/images/emoji/basketball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png
deleted file mode 100644
index 8ce90c5cad6..00000000000
--- a/app/assets/images/emoji/basketball_player.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png
deleted file mode 100644
index cd12c7ab9bf..00000000000
--- a/app/assets/images/emoji/basketball_player_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png
deleted file mode 100644
index f892fd596da..00000000000
--- a/app/assets/images/emoji/basketball_player_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png
deleted file mode 100644
index e109997a91a..00000000000
--- a/app/assets/images/emoji/basketball_player_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png
deleted file mode 100644
index 3b90b946af4..00000000000
--- a/app/assets/images/emoji/basketball_player_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png
deleted file mode 100644
index bafed7828a7..00000000000
--- a/app/assets/images/emoji/basketball_player_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png
deleted file mode 100644
index 3152c047e00..00000000000
--- a/app/assets/images/emoji/bat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png
deleted file mode 100644
index 43fba5c8a28..00000000000
--- a/app/assets/images/emoji/bath.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png
deleted file mode 100644
index 2152eabf2f5..00000000000
--- a/app/assets/images/emoji/bath_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png
deleted file mode 100644
index 2102e6133e3..00000000000
--- a/app/assets/images/emoji/bath_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png
deleted file mode 100644
index fae66181e9f..00000000000
--- a/app/assets/images/emoji/bath_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png
deleted file mode 100644
index 1f8959d0d99..00000000000
--- a/app/assets/images/emoji/bath_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png
deleted file mode 100644
index c8a08e84f25..00000000000
--- a/app/assets/images/emoji/bath_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png
deleted file mode 100644
index 9a5f09361eb..00000000000
--- a/app/assets/images/emoji/bathtub.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png
deleted file mode 100644
index f593e2bdb65..00000000000
--- a/app/assets/images/emoji/battery.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png
deleted file mode 100644
index 69108c8ea10..00000000000
--- a/app/assets/images/emoji/beach.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png
deleted file mode 100644
index 220a74f8132..00000000000
--- a/app/assets/images/emoji/beach_umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png
deleted file mode 100644
index 272d56bbbcc..00000000000
--- a/app/assets/images/emoji/bear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png
deleted file mode 100644
index 86f964e245d..00000000000
--- a/app/assets/images/emoji/bed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png
deleted file mode 100644
index 46156060096..00000000000
--- a/app/assets/images/emoji/bee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png
deleted file mode 100644
index b6d73dc0b7a..00000000000
--- a/app/assets/images/emoji/beer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png
deleted file mode 100644
index b55deb66b41..00000000000
--- a/app/assets/images/emoji/beers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png
deleted file mode 100644
index 3d93174d7fc..00000000000
--- a/app/assets/images/emoji/beetle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png
deleted file mode 100644
index bc434fb7cb5..00000000000
--- a/app/assets/images/emoji/beginner.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png
deleted file mode 100644
index 5b3b0461999..00000000000
--- a/app/assets/images/emoji/bell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png
deleted file mode 100644
index 6b3297ceaf7..00000000000
--- a/app/assets/images/emoji/bellhop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png
deleted file mode 100644
index 83d41ca7eb9..00000000000
--- a/app/assets/images/emoji/bento.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png
deleted file mode 100644
index 9274da11048..00000000000
--- a/app/assets/images/emoji/bicyclist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png
deleted file mode 100644
index decc2f728fe..00000000000
--- a/app/assets/images/emoji/bicyclist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png
deleted file mode 100644
index 0067717b80a..00000000000
--- a/app/assets/images/emoji/bicyclist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png
deleted file mode 100644
index a4f7b5e2776..00000000000
--- a/app/assets/images/emoji/bicyclist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png
deleted file mode 100644
index a3c8a797db4..00000000000
--- a/app/assets/images/emoji/bicyclist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png
deleted file mode 100644
index 1606a874051..00000000000
--- a/app/assets/images/emoji/bicyclist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png
deleted file mode 100644
index 556ed70f1a7..00000000000
--- a/app/assets/images/emoji/bike.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png
deleted file mode 100644
index 77a8a0aae5b..00000000000
--- a/app/assets/images/emoji/bikini.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png
deleted file mode 100644
index 007b4fc2d85..00000000000
--- a/app/assets/images/emoji/biohazard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png
deleted file mode 100644
index e201c22be33..00000000000
--- a/app/assets/images/emoji/bird.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png
deleted file mode 100644
index 317e9a41949..00000000000
--- a/app/assets/images/emoji/birthday.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png
deleted file mode 100644
index b62b87170e8..00000000000
--- a/app/assets/images/emoji/black_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png
deleted file mode 100644
index b4068c3e6e8..00000000000
--- a/app/assets/images/emoji/black_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png
deleted file mode 100644
index 3d0924b68aa..00000000000
--- a/app/assets/images/emoji/black_joker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png
deleted file mode 100644
index 162f2bb4290..00000000000
--- a/app/assets/images/emoji/black_large_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png
deleted file mode 100644
index 39765bba610..00000000000
--- a/app/assets/images/emoji/black_medium_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png
deleted file mode 100644
index 05a30a6aa2d..00000000000
--- a/app/assets/images/emoji/black_medium_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png
deleted file mode 100644
index 872d0ae1598..00000000000
--- a/app/assets/images/emoji/black_nib.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png
deleted file mode 100644
index 48595d3e1a9..00000000000
--- a/app/assets/images/emoji/black_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png
deleted file mode 100644
index a78fc2f6b63..00000000000
--- a/app/assets/images/emoji/black_square_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png
deleted file mode 100644
index 4083026c157..00000000000
--- a/app/assets/images/emoji/blossom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png
deleted file mode 100644
index a10f4f84e35..00000000000
--- a/app/assets/images/emoji/blowfish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png
deleted file mode 100644
index e1e455401cc..00000000000
--- a/app/assets/images/emoji/blue_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png
deleted file mode 100644
index e8ba817d393..00000000000
--- a/app/assets/images/emoji/blue_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png
deleted file mode 100644
index bdf1287e55e..00000000000
--- a/app/assets/images/emoji/blue_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png
deleted file mode 100644
index aac1a424ad4..00000000000
--- a/app/assets/images/emoji/blush.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png
deleted file mode 100644
index fead972633c..00000000000
--- a/app/assets/images/emoji/boar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png
deleted file mode 100644
index c7f8f81c939..00000000000
--- a/app/assets/images/emoji/bomb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png
deleted file mode 100644
index 0f4447ed396..00000000000
--- a/app/assets/images/emoji/book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png
deleted file mode 100644
index bbb444611f0..00000000000
--- a/app/assets/images/emoji/bookmark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png
deleted file mode 100644
index f8d9e01b428..00000000000
--- a/app/assets/images/emoji/bookmark_tabs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png
deleted file mode 100644
index 59a8bafeb0d..00000000000
--- a/app/assets/images/emoji/books.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png
deleted file mode 100644
index 9b0f027b1a8..00000000000
--- a/app/assets/images/emoji/boom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png
deleted file mode 100644
index 11f1065ed07..00000000000
--- a/app/assets/images/emoji/boot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png
deleted file mode 100644
index 11455af6df4..00000000000
--- a/app/assets/images/emoji/bouquet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png
deleted file mode 100644
index d8f793088dc..00000000000
--- a/app/assets/images/emoji/bow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png
deleted file mode 100644
index 6a538bf475f..00000000000
--- a/app/assets/images/emoji/bow_and_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png
deleted file mode 100644
index 87afb7b54cf..00000000000
--- a/app/assets/images/emoji/bow_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png
deleted file mode 100644
index 3ccf7dc0850..00000000000
--- a/app/assets/images/emoji/bow_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png
deleted file mode 100644
index 8b9eb64f926..00000000000
--- a/app/assets/images/emoji/bow_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png
deleted file mode 100644
index 683795ff40d..00000000000
--- a/app/assets/images/emoji/bow_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png
deleted file mode 100644
index 7969d971752..00000000000
--- a/app/assets/images/emoji/bow_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png
deleted file mode 100644
index 63add89e53b..00000000000
--- a/app/assets/images/emoji/bowling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png
deleted file mode 100644
index 9838f24e51a..00000000000
--- a/app/assets/images/emoji/boxing_glove.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png
deleted file mode 100644
index 8ecfb0a4e92..00000000000
--- a/app/assets/images/emoji/boy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png
deleted file mode 100644
index 2fc436ea512..00000000000
--- a/app/assets/images/emoji/boy_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png
deleted file mode 100644
index 09a5f18d360..00000000000
--- a/app/assets/images/emoji/boy_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png
deleted file mode 100644
index 3cfe675dd3a..00000000000
--- a/app/assets/images/emoji/boy_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png
deleted file mode 100644
index 780be0ace36..00000000000
--- a/app/assets/images/emoji/boy_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png
deleted file mode 100644
index f32fe22e35c..00000000000
--- a/app/assets/images/emoji/boy_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png
deleted file mode 100644
index 6676510aaa5..00000000000
--- a/app/assets/images/emoji/bread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png
deleted file mode 100644
index eaf4bd97890..00000000000
--- a/app/assets/images/emoji/bride_with_veil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png
deleted file mode 100644
index c4fb141ae8f..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png
deleted file mode 100644
index c248769fc06..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png
deleted file mode 100644
index 962c0a6eedb..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png
deleted file mode 100644
index 740ca208cd4..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png
deleted file mode 100644
index 5cc5598587d..00000000000
--- a/app/assets/images/emoji/bride_with_veil_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png
deleted file mode 100644
index 1d444e0be65..00000000000
--- a/app/assets/images/emoji/bridge_at_night.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png
deleted file mode 100644
index b9912ba2148..00000000000
--- a/app/assets/images/emoji/briefcase.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png
deleted file mode 100644
index 718e26ee122..00000000000
--- a/app/assets/images/emoji/broken_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png
deleted file mode 100644
index e64e72f259a..00000000000
--- a/app/assets/images/emoji/bug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png
deleted file mode 100644
index 38e32e02d9f..00000000000
--- a/app/assets/images/emoji/bulb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png
deleted file mode 100644
index 4f698e056fa..00000000000
--- a/app/assets/images/emoji/bullettrain_front.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png
deleted file mode 100644
index ed61c67bf07..00000000000
--- a/app/assets/images/emoji/bullettrain_side.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png
deleted file mode 100644
index 02bd5601df7..00000000000
--- a/app/assets/images/emoji/burrito.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png
deleted file mode 100644
index 641ddc56ca7..00000000000
--- a/app/assets/images/emoji/bus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png
deleted file mode 100644
index b2b62208bfd..00000000000
--- a/app/assets/images/emoji/busstop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png
deleted file mode 100644
index 123b2cbe1fb..00000000000
--- a/app/assets/images/emoji/bust_in_silhouette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png
deleted file mode 100644
index d7656860a1c..00000000000
--- a/app/assets/images/emoji/busts_in_silhouette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png
deleted file mode 100644
index 5631fe99226..00000000000
--- a/app/assets/images/emoji/butterfly.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png
deleted file mode 100644
index 9b48ccf3d0c..00000000000
--- a/app/assets/images/emoji/cactus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png
deleted file mode 100644
index 4368177be9a..00000000000
--- a/app/assets/images/emoji/cake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png
deleted file mode 100644
index 47353b74447..00000000000
--- a/app/assets/images/emoji/calendar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png
deleted file mode 100644
index dec8d49bfa8..00000000000
--- a/app/assets/images/emoji/calendar_spiral.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png
deleted file mode 100644
index a10c59ba711..00000000000
--- a/app/assets/images/emoji/call_me.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png
deleted file mode 100644
index 2c93201181a..00000000000
--- a/app/assets/images/emoji/call_me_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png
deleted file mode 100644
index c39f45a41ed..00000000000
--- a/app/assets/images/emoji/call_me_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png
deleted file mode 100644
index 83a57f63c29..00000000000
--- a/app/assets/images/emoji/call_me_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png
deleted file mode 100644
index 65b3468fe44..00000000000
--- a/app/assets/images/emoji/call_me_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png
deleted file mode 100644
index 94ef68ff3b3..00000000000
--- a/app/assets/images/emoji/call_me_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png
deleted file mode 100644
index e2f308f8e46..00000000000
--- a/app/assets/images/emoji/calling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png
deleted file mode 100644
index b421d07a805..00000000000
--- a/app/assets/images/emoji/camel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png
deleted file mode 100644
index 0a3429f72ef..00000000000
--- a/app/assets/images/emoji/camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png
deleted file mode 100644
index 27471da2029..00000000000
--- a/app/assets/images/emoji/camera_with_flash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png
deleted file mode 100644
index d589cc1f44b..00000000000
--- a/app/assets/images/emoji/camping.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png
deleted file mode 100644
index a64af07cb5f..00000000000
--- a/app/assets/images/emoji/cancer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png
deleted file mode 100644
index 0b56444e355..00000000000
--- a/app/assets/images/emoji/candle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png
deleted file mode 100644
index 8c67ace3a35..00000000000
--- a/app/assets/images/emoji/candy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png
deleted file mode 100644
index e26cdb9da69..00000000000
--- a/app/assets/images/emoji/canoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png
deleted file mode 100644
index fe9482d2d8a..00000000000
--- a/app/assets/images/emoji/capital_abcd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png
deleted file mode 100644
index 6293d31d4b1..00000000000
--- a/app/assets/images/emoji/capricorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png
deleted file mode 100644
index f2e764ce59d..00000000000
--- a/app/assets/images/emoji/card_box.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png
deleted file mode 100644
index 151e11cb3b4..00000000000
--- a/app/assets/images/emoji/card_index.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png
deleted file mode 100644
index a17074edf05..00000000000
--- a/app/assets/images/emoji/carousel_horse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png
deleted file mode 100644
index c68829b58e7..00000000000
--- a/app/assets/images/emoji/carrot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png
deleted file mode 100644
index cbcaa578253..00000000000
--- a/app/assets/images/emoji/cartwheel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png
deleted file mode 100644
index db6d65895fb..00000000000
--- a/app/assets/images/emoji/cartwheel_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png
deleted file mode 100644
index e00ffbc27a8..00000000000
--- a/app/assets/images/emoji/cartwheel_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png
deleted file mode 100644
index 49321be391f..00000000000
--- a/app/assets/images/emoji/cartwheel_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png
deleted file mode 100644
index d4562b5e3dd..00000000000
--- a/app/assets/images/emoji/cartwheel_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png
deleted file mode 100644
index 6e09a870767..00000000000
--- a/app/assets/images/emoji/cartwheel_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png
deleted file mode 100644
index efd82c2abf3..00000000000
--- a/app/assets/images/emoji/cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png
deleted file mode 100644
index 46abe8cbc14..00000000000
--- a/app/assets/images/emoji/cat2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png
deleted file mode 100644
index e6b01449cd9..00000000000
--- a/app/assets/images/emoji/cd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png
deleted file mode 100644
index 57f46139a06..00000000000
--- a/app/assets/images/emoji/chains.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png
deleted file mode 100644
index 285a79a93d0..00000000000
--- a/app/assets/images/emoji/champagne.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png
deleted file mode 100644
index 31937ae9392..00000000000
--- a/app/assets/images/emoji/champagne_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png
deleted file mode 100644
index 9773f03be22..00000000000
--- a/app/assets/images/emoji/chart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png
deleted file mode 100644
index 5222ec72d85..00000000000
--- a/app/assets/images/emoji/chart_with_downwards_trend.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png
deleted file mode 100644
index f13cfcf9956..00000000000
--- a/app/assets/images/emoji/chart_with_upwards_trend.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png
deleted file mode 100644
index 5a71eecb89b..00000000000
--- a/app/assets/images/emoji/checkered_flag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png
deleted file mode 100644
index 00e99762286..00000000000
--- a/app/assets/images/emoji/cheese.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png
deleted file mode 100644
index 9b10cbaac5e..00000000000
--- a/app/assets/images/emoji/cherries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png
deleted file mode 100644
index 282f3e7bc81..00000000000
--- a/app/assets/images/emoji/cherry_blossom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png
deleted file mode 100644
index e9fb40468ed..00000000000
--- a/app/assets/images/emoji/chestnut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png
deleted file mode 100644
index 9a6992e55ba..00000000000
--- a/app/assets/images/emoji/chicken.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png
deleted file mode 100644
index fa4c091c7c3..00000000000
--- a/app/assets/images/emoji/children_crossing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png
deleted file mode 100644
index 2aac560cb22..00000000000
--- a/app/assets/images/emoji/chipmunk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png
deleted file mode 100644
index 318bbd40ef9..00000000000
--- a/app/assets/images/emoji/chocolate_bar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png
deleted file mode 100644
index 4197d37a52b..00000000000
--- a/app/assets/images/emoji/christmas_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png
deleted file mode 100644
index 8242fd272b3..00000000000
--- a/app/assets/images/emoji/church.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png
deleted file mode 100644
index 65f27b386f2..00000000000
--- a/app/assets/images/emoji/cinema.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png
deleted file mode 100644
index b0379775b12..00000000000
--- a/app/assets/images/emoji/circus_tent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png
deleted file mode 100644
index 80cdff7cf5d..00000000000
--- a/app/assets/images/emoji/city_dusk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png
deleted file mode 100644
index 7cded0ba55b..00000000000
--- a/app/assets/images/emoji/city_sunset.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png
deleted file mode 100644
index d7b9844a0b4..00000000000
--- a/app/assets/images/emoji/cityscape.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png
deleted file mode 100644
index 8b01b4343e2..00000000000
--- a/app/assets/images/emoji/cl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png
deleted file mode 100644
index b0ffe928920..00000000000
--- a/app/assets/images/emoji/clap.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png
deleted file mode 100644
index de4bc837b96..00000000000
--- a/app/assets/images/emoji/clap_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png
deleted file mode 100644
index 1323de775ba..00000000000
--- a/app/assets/images/emoji/clap_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png
deleted file mode 100644
index d448ca19dde..00000000000
--- a/app/assets/images/emoji/clap_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png
deleted file mode 100644
index c49f44ee91d..00000000000
--- a/app/assets/images/emoji/clap_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png
deleted file mode 100644
index 29ee9bdf37c..00000000000
--- a/app/assets/images/emoji/clap_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png
deleted file mode 100644
index 81390883111..00000000000
--- a/app/assets/images/emoji/clapper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png
deleted file mode 100644
index de7b559daaf..00000000000
--- a/app/assets/images/emoji/classical_building.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png
deleted file mode 100644
index 7edcfc52509..00000000000
--- a/app/assets/images/emoji/clipboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png
deleted file mode 100644
index ffdb451e3a8..00000000000
--- a/app/assets/images/emoji/clock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png
deleted file mode 100644
index d6e34941f23..00000000000
--- a/app/assets/images/emoji/clock1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png
deleted file mode 100644
index e62b245cdbe..00000000000
--- a/app/assets/images/emoji/clock10.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png
deleted file mode 100644
index 0802b3c65b9..00000000000
--- a/app/assets/images/emoji/clock1030.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png
deleted file mode 100644
index 0983345273b..00000000000
--- a/app/assets/images/emoji/clock11.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png
deleted file mode 100644
index d970d03b809..00000000000
--- a/app/assets/images/emoji/clock1130.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png
deleted file mode 100644
index e61caa4b3e2..00000000000
--- a/app/assets/images/emoji/clock12.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png
deleted file mode 100644
index f2b1d261721..00000000000
--- a/app/assets/images/emoji/clock1230.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png
deleted file mode 100644
index 86b7689b84e..00000000000
--- a/app/assets/images/emoji/clock130.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png
deleted file mode 100644
index a54253d7d57..00000000000
--- a/app/assets/images/emoji/clock2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png
deleted file mode 100644
index 7a787e018e6..00000000000
--- a/app/assets/images/emoji/clock230.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png
deleted file mode 100644
index 27ec4b1f514..00000000000
--- a/app/assets/images/emoji/clock3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png
deleted file mode 100644
index c6860395cec..00000000000
--- a/app/assets/images/emoji/clock330.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png
deleted file mode 100644
index 60a1ef4cc13..00000000000
--- a/app/assets/images/emoji/clock4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png
deleted file mode 100644
index 3c05b362122..00000000000
--- a/app/assets/images/emoji/clock430.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png
deleted file mode 100644
index c9382d1e094..00000000000
--- a/app/assets/images/emoji/clock5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png
deleted file mode 100644
index c21fa926db2..00000000000
--- a/app/assets/images/emoji/clock530.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png
deleted file mode 100644
index 8fd5d3f5bd7..00000000000
--- a/app/assets/images/emoji/clock6.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png
deleted file mode 100644
index 2aec87fefcf..00000000000
--- a/app/assets/images/emoji/clock630.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png
deleted file mode 100644
index 8c7084036f2..00000000000
--- a/app/assets/images/emoji/clock7.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png
deleted file mode 100644
index f7a1135e03f..00000000000
--- a/app/assets/images/emoji/clock730.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png
deleted file mode 100644
index fcddf722e95..00000000000
--- a/app/assets/images/emoji/clock8.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png
deleted file mode 100644
index 799b4aebc08..00000000000
--- a/app/assets/images/emoji/clock830.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png
deleted file mode 100644
index dfbe0117981..00000000000
--- a/app/assets/images/emoji/clock9.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png
deleted file mode 100644
index 4a2092ee6f0..00000000000
--- a/app/assets/images/emoji/clock930.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png
deleted file mode 100644
index 6395cf2151e..00000000000
--- a/app/assets/images/emoji/closed_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png
deleted file mode 100644
index 1c1cd5d0741..00000000000
--- a/app/assets/images/emoji/closed_lock_with_key.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png
deleted file mode 100644
index ecefba9e446..00000000000
--- a/app/assets/images/emoji/closed_umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png
deleted file mode 100644
index 5b4f57f77ba..00000000000
--- a/app/assets/images/emoji/cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png
deleted file mode 100644
index 0831e88aa31..00000000000
--- a/app/assets/images/emoji/cloud_lightning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png
deleted file mode 100644
index 385685e0512..00000000000
--- a/app/assets/images/emoji/cloud_rain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png
deleted file mode 100644
index 9720384eb99..00000000000
--- a/app/assets/images/emoji/cloud_snow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png
deleted file mode 100644
index 4821c89da1e..00000000000
--- a/app/assets/images/emoji/cloud_tornado.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png
deleted file mode 100644
index 02b7ff70049..00000000000
--- a/app/assets/images/emoji/clown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png
deleted file mode 100644
index 4f2abf791ca..00000000000
--- a/app/assets/images/emoji/clubs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png
deleted file mode 100644
index 2e50c57e98d..00000000000
--- a/app/assets/images/emoji/cocktail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png
deleted file mode 100644
index 553061471b1..00000000000
--- a/app/assets/images/emoji/coffee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png
deleted file mode 100644
index fb2932aa5f6..00000000000
--- a/app/assets/images/emoji/coffin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png
deleted file mode 100644
index 85b2231bbf6..00000000000
--- a/app/assets/images/emoji/cold_sweat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png
deleted file mode 100644
index a99751f79be..00000000000
--- a/app/assets/images/emoji/comet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png
deleted file mode 100644
index d7eda7f362a..00000000000
--- a/app/assets/images/emoji/compression.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png
deleted file mode 100644
index c1fee27e3a9..00000000000
--- a/app/assets/images/emoji/computer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png
deleted file mode 100644
index ba4fd9b12be..00000000000
--- a/app/assets/images/emoji/confetti_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png
deleted file mode 100644
index aa4b29e9375..00000000000
--- a/app/assets/images/emoji/confounded.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png
deleted file mode 100644
index 502b6bf0e0b..00000000000
--- a/app/assets/images/emoji/confused.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png
deleted file mode 100644
index ba8c89d95ee..00000000000
--- a/app/assets/images/emoji/congratulations.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png
deleted file mode 100644
index ef8db5f471c..00000000000
--- a/app/assets/images/emoji/construction.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png
deleted file mode 100644
index 8206a20f63f..00000000000
--- a/app/assets/images/emoji/construction_site.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png
deleted file mode 100644
index a9970a89005..00000000000
--- a/app/assets/images/emoji/construction_worker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png
deleted file mode 100644
index 2f24a2bab24..00000000000
--- a/app/assets/images/emoji/construction_worker_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png
deleted file mode 100644
index 93c8fec5a75..00000000000
--- a/app/assets/images/emoji/construction_worker_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png
deleted file mode 100644
index abc1f2af2e0..00000000000
--- a/app/assets/images/emoji/construction_worker_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png
deleted file mode 100644
index eed83289aeb..00000000000
--- a/app/assets/images/emoji/construction_worker_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png
deleted file mode 100644
index acbb220b8bb..00000000000
--- a/app/assets/images/emoji/construction_worker_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png
deleted file mode 100644
index 6635ac93b50..00000000000
--- a/app/assets/images/emoji/control_knobs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png
deleted file mode 100644
index 26b53b5669e..00000000000
--- a/app/assets/images/emoji/convenience_store.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png
deleted file mode 100644
index 1b6bcb1554f..00000000000
--- a/app/assets/images/emoji/cookie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png
deleted file mode 100644
index 918c980577a..00000000000
--- a/app/assets/images/emoji/cooking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png
deleted file mode 100644
index 74674978d00..00000000000
--- a/app/assets/images/emoji/cool.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png
deleted file mode 100644
index 0b16d7c17b7..00000000000
--- a/app/assets/images/emoji/cop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png
deleted file mode 100644
index 6ccba3879dc..00000000000
--- a/app/assets/images/emoji/cop_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png
deleted file mode 100644
index 7814ea9f52d..00000000000
--- a/app/assets/images/emoji/cop_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png
deleted file mode 100644
index d78e88ec872..00000000000
--- a/app/assets/images/emoji/cop_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png
deleted file mode 100644
index 2e13c508315..00000000000
--- a/app/assets/images/emoji/cop_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png
deleted file mode 100644
index 2980d61cc2e..00000000000
--- a/app/assets/images/emoji/cop_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png
deleted file mode 100644
index 6b9a6adbfd2..00000000000
--- a/app/assets/images/emoji/copyright.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png
deleted file mode 100644
index 36e20127931..00000000000
--- a/app/assets/images/emoji/corn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png
deleted file mode 100644
index 27b19b13bb0..00000000000
--- a/app/assets/images/emoji/couch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png
deleted file mode 100644
index 960323f3c16..00000000000
--- a/app/assets/images/emoji/couple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png
deleted file mode 100644
index 8759fa5db87..00000000000
--- a/app/assets/images/emoji/couple_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png
deleted file mode 100644
index 62111601b36..00000000000
--- a/app/assets/images/emoji/couple_with_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png
deleted file mode 100644
index 08fdabcdc5c..00000000000
--- a/app/assets/images/emoji/couple_ww.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png
deleted file mode 100644
index 9aa519da9e8..00000000000
--- a/app/assets/images/emoji/couplekiss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png
deleted file mode 100644
index 718a3986d64..00000000000
--- a/app/assets/images/emoji/cow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png
deleted file mode 100644
index 4d0ca534ff1..00000000000
--- a/app/assets/images/emoji/cow2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png
deleted file mode 100644
index 70dd5d0d9d1..00000000000
--- a/app/assets/images/emoji/cowboy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png
deleted file mode 100644
index 19f3047ab61..00000000000
--- a/app/assets/images/emoji/crab.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png
deleted file mode 100644
index 8d7b427aaa3..00000000000
--- a/app/assets/images/emoji/crayon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png
deleted file mode 100644
index 372777d5c61..00000000000
--- a/app/assets/images/emoji/credit_card.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png
deleted file mode 100644
index 765420ecec7..00000000000
--- a/app/assets/images/emoji/crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png
deleted file mode 100644
index d602294a2cd..00000000000
--- a/app/assets/images/emoji/cricket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png
deleted file mode 100644
index 3005c46f176..00000000000
--- a/app/assets/images/emoji/crocodile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png
deleted file mode 100644
index fb33feb1a38..00000000000
--- a/app/assets/images/emoji/croissant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png
deleted file mode 100644
index 42b10e82257..00000000000
--- a/app/assets/images/emoji/cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png
deleted file mode 100644
index 273bd0f0fe5..00000000000
--- a/app/assets/images/emoji/crossed_flags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png
deleted file mode 100644
index 907e9607134..00000000000
--- a/app/assets/images/emoji/crossed_swords.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png
deleted file mode 100644
index 93b82d92f04..00000000000
--- a/app/assets/images/emoji/crown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png
deleted file mode 100644
index 19d4acbe40c..00000000000
--- a/app/assets/images/emoji/cruise_ship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png
deleted file mode 100644
index b7877f8a173..00000000000
--- a/app/assets/images/emoji/cry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png
deleted file mode 100644
index b4f49715e00..00000000000
--- a/app/assets/images/emoji/crying_cat_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png
deleted file mode 100644
index 485d5c888f1..00000000000
--- a/app/assets/images/emoji/crystal_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png
deleted file mode 100644
index 500807059d2..00000000000
--- a/app/assets/images/emoji/cucumber.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png
deleted file mode 100644
index 2df0078ddd1..00000000000
--- a/app/assets/images/emoji/cupid.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png
deleted file mode 100644
index 440aa56d50e..00000000000
--- a/app/assets/images/emoji/curly_loop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png
deleted file mode 100644
index 4d46c6050e7..00000000000
--- a/app/assets/images/emoji/currency_exchange.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png
deleted file mode 100644
index 69657ca8103..00000000000
--- a/app/assets/images/emoji/curry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png
deleted file mode 100644
index fa3df67b8f6..00000000000
--- a/app/assets/images/emoji/custard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png
deleted file mode 100644
index 21b7ce2c69e..00000000000
--- a/app/assets/images/emoji/customs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png
deleted file mode 100644
index ff00b1afe70..00000000000
--- a/app/assets/images/emoji/cyclone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png
deleted file mode 100644
index 66e97b0aa25..00000000000
--- a/app/assets/images/emoji/dagger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png
deleted file mode 100644
index 04b166991cb..00000000000
--- a/app/assets/images/emoji/dancer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png
deleted file mode 100644
index 2c7b11c3a6e..00000000000
--- a/app/assets/images/emoji/dancer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png
deleted file mode 100644
index cb04b1f907e..00000000000
--- a/app/assets/images/emoji/dancer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png
deleted file mode 100644
index 98c5bca7b64..00000000000
--- a/app/assets/images/emoji/dancer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png
deleted file mode 100644
index fdb1e00cbba..00000000000
--- a/app/assets/images/emoji/dancer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png
deleted file mode 100644
index 0e34e0e23f0..00000000000
--- a/app/assets/images/emoji/dancer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png
deleted file mode 100644
index 67e6ffacb76..00000000000
--- a/app/assets/images/emoji/dancers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png
deleted file mode 100644
index f73f37b01c7..00000000000
--- a/app/assets/images/emoji/dango.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png
deleted file mode 100644
index b1b6db0acff..00000000000
--- a/app/assets/images/emoji/dark_sunglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png
deleted file mode 100644
index f6704aeb8ba..00000000000
--- a/app/assets/images/emoji/dart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png
deleted file mode 100644
index 064b8525c12..00000000000
--- a/app/assets/images/emoji/dash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png
deleted file mode 100644
index f05b3da97b8..00000000000
--- a/app/assets/images/emoji/date.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png
deleted file mode 100644
index 785fc1c30ea..00000000000
--- a/app/assets/images/emoji/deciduous_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png
deleted file mode 100644
index d8698195ff0..00000000000
--- a/app/assets/images/emoji/deer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png
deleted file mode 100644
index 58867c7a6e1..00000000000
--- a/app/assets/images/emoji/department_store.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png
deleted file mode 100644
index e9966ff8c65..00000000000
--- a/app/assets/images/emoji/desert.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png
deleted file mode 100644
index 909bd42b5e1..00000000000
--- a/app/assets/images/emoji/desktop.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 2a22a26d1e2..00000000000
--- a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png
deleted file mode 100644
index 1f25f51f97a..00000000000
--- a/app/assets/images/emoji/diamonds.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png
deleted file mode 100644
index efe4e67e23c..00000000000
--- a/app/assets/images/emoji/disappointed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png
deleted file mode 100644
index aef864d2b3d..00000000000
--- a/app/assets/images/emoji/disappointed_relieved.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png
deleted file mode 100644
index 46a7e403f9d..00000000000
--- a/app/assets/images/emoji/dividers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png
deleted file mode 100644
index 85f52efad24..00000000000
--- a/app/assets/images/emoji/dizzy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png
deleted file mode 100644
index 3120316ab5e..00000000000
--- a/app/assets/images/emoji/dizzy_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png
deleted file mode 100644
index 341d2575f4f..00000000000
--- a/app/assets/images/emoji/do_not_litter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png
deleted file mode 100644
index 281b81d58bd..00000000000
--- a/app/assets/images/emoji/dog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png
deleted file mode 100644
index 976143dbdbe..00000000000
--- a/app/assets/images/emoji/dog2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png
deleted file mode 100644
index a9904c28293..00000000000
--- a/app/assets/images/emoji/dollar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png
deleted file mode 100644
index 10955615110..00000000000
--- a/app/assets/images/emoji/dolls.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png
deleted file mode 100644
index 81434809003..00000000000
--- a/app/assets/images/emoji/dolphin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png
deleted file mode 100644
index 36ae3e27494..00000000000
--- a/app/assets/images/emoji/door.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png
deleted file mode 100644
index 0ca4cd0bde8..00000000000
--- a/app/assets/images/emoji/doughnut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png
deleted file mode 100644
index 9580c4917d7..00000000000
--- a/app/assets/images/emoji/dove.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png
deleted file mode 100644
index d6311cf5429..00000000000
--- a/app/assets/images/emoji/dragon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png
deleted file mode 100644
index 3c2720446c6..00000000000
--- a/app/assets/images/emoji/dragon_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png
deleted file mode 100644
index a697ca5c57d..00000000000
--- a/app/assets/images/emoji/dress.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png
deleted file mode 100644
index 5271637c7c4..00000000000
--- a/app/assets/images/emoji/dromedary_camel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png
deleted file mode 100644
index a5460532597..00000000000
--- a/app/assets/images/emoji/drooling_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png
deleted file mode 100644
index 71241ec3061..00000000000
--- a/app/assets/images/emoji/droplet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png
deleted file mode 100644
index b038727cc99..00000000000
--- a/app/assets/images/emoji/drum.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png
deleted file mode 100644
index 74330b77ca3..00000000000
--- a/app/assets/images/emoji/duck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png
deleted file mode 100644
index 045a6f7a08d..00000000000
--- a/app/assets/images/emoji/dvd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png
deleted file mode 100644
index d22e654a20b..00000000000
--- a/app/assets/images/emoji/e-mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png
deleted file mode 100644
index 4f277debeef..00000000000
--- a/app/assets/images/emoji/eagle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png
deleted file mode 100644
index f84f9ff154a..00000000000
--- a/app/assets/images/emoji/ear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png
deleted file mode 100644
index 3564d9d643a..00000000000
--- a/app/assets/images/emoji/ear_of_rice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png
deleted file mode 100644
index d09e1e41996..00000000000
--- a/app/assets/images/emoji/ear_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png
deleted file mode 100644
index 300d60a9948..00000000000
--- a/app/assets/images/emoji/ear_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png
deleted file mode 100644
index 2a56eebe445..00000000000
--- a/app/assets/images/emoji/ear_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png
deleted file mode 100644
index bd270f7763e..00000000000
--- a/app/assets/images/emoji/ear_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png
deleted file mode 100644
index b96bb441dff..00000000000
--- a/app/assets/images/emoji/ear_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png
deleted file mode 100644
index 66c3348c23a..00000000000
--- a/app/assets/images/emoji/earth_africa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png
deleted file mode 100644
index 538c3cddd68..00000000000
--- a/app/assets/images/emoji/earth_americas.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png
deleted file mode 100644
index d8df97fec3c..00000000000
--- a/app/assets/images/emoji/earth_asia.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png
deleted file mode 100644
index c171974d993..00000000000
--- a/app/assets/images/emoji/egg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png
deleted file mode 100644
index fafd7c1a14c..00000000000
--- a/app/assets/images/emoji/eggplant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png
deleted file mode 100644
index 8c95874d4c5..00000000000
--- a/app/assets/images/emoji/eight.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png
deleted file mode 100644
index 820179bda50..00000000000
--- a/app/assets/images/emoji/eight_pointed_black_star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png
deleted file mode 100644
index 3307ffa62ee..00000000000
--- a/app/assets/images/emoji/eight_spoked_asterisk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png
deleted file mode 100644
index ec5cfc48973..00000000000
--- a/app/assets/images/emoji/eject.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png
deleted file mode 100644
index 31d1eb215b4..00000000000
--- a/app/assets/images/emoji/electric_plug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png
deleted file mode 100644
index b8a6d140595..00000000000
--- a/app/assets/images/emoji/elephant.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png
deleted file mode 100644
index ef3ccd5f367..00000000000
--- a/app/assets/images/emoji/end.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png
deleted file mode 100644
index ec77ac375a4..00000000000
--- a/app/assets/images/emoji/envelope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png
deleted file mode 100644
index 7448a6b7673..00000000000
--- a/app/assets/images/emoji/envelope_with_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png
deleted file mode 100644
index a49020820e1..00000000000
--- a/app/assets/images/emoji/euro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png
deleted file mode 100644
index 888d11332ce..00000000000
--- a/app/assets/images/emoji/european_castle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png
deleted file mode 100644
index 3745aff8dd2..00000000000
--- a/app/assets/images/emoji/european_post_office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png
deleted file mode 100644
index f679d8dd772..00000000000
--- a/app/assets/images/emoji/evergreen_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png
deleted file mode 100644
index 2c14406422f..00000000000
--- a/app/assets/images/emoji/exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png
deleted file mode 100644
index 2954017f6c2..00000000000
--- a/app/assets/images/emoji/expressionless.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png
deleted file mode 100644
index 9d989cdd375..00000000000
--- a/app/assets/images/emoji/eye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png
deleted file mode 100644
index 21bd22bbcce..00000000000
--- a/app/assets/images/emoji/eye_in_speech_bubble.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png
deleted file mode 100644
index 865d8274acf..00000000000
--- a/app/assets/images/emoji/eyeglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png
deleted file mode 100644
index 2102ada7e09..00000000000
--- a/app/assets/images/emoji/eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png
deleted file mode 100644
index defc796cf16..00000000000
--- a/app/assets/images/emoji/face_palm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png
deleted file mode 100644
index 2f4b010bb40..00000000000
--- a/app/assets/images/emoji/face_palm_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png
deleted file mode 100644
index 97fb6831687..00000000000
--- a/app/assets/images/emoji/face_palm_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png
deleted file mode 100644
index b5b5c1e5306..00000000000
--- a/app/assets/images/emoji/face_palm_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png
deleted file mode 100644
index 2840b113483..00000000000
--- a/app/assets/images/emoji/face_palm_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png
deleted file mode 100644
index 6f070db98be..00000000000
--- a/app/assets/images/emoji/face_palm_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png
deleted file mode 100644
index e1d2ddf4a27..00000000000
--- a/app/assets/images/emoji/factory.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png
deleted file mode 100644
index 0d60e7bdf2d..00000000000
--- a/app/assets/images/emoji/fallen_leaf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png
deleted file mode 100644
index 26421965791..00000000000
--- a/app/assets/images/emoji/family.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png
deleted file mode 100644
index 7a2e4e2c491..00000000000
--- a/app/assets/images/emoji/family_mmb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png
deleted file mode 100644
index 81e6c0fc0ee..00000000000
--- a/app/assets/images/emoji/family_mmbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png
deleted file mode 100644
index 932a85e1fe5..00000000000
--- a/app/assets/images/emoji/family_mmg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png
deleted file mode 100644
index 41e35166670..00000000000
--- a/app/assets/images/emoji/family_mmgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png
deleted file mode 100644
index 8e8ccfe6c7f..00000000000
--- a/app/assets/images/emoji/family_mmgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png
deleted file mode 100644
index b544fbe573f..00000000000
--- a/app/assets/images/emoji/family_mwbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png
deleted file mode 100644
index 71d2681c32a..00000000000
--- a/app/assets/images/emoji/family_mwg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png
deleted file mode 100644
index 40dbf1f7a18..00000000000
--- a/app/assets/images/emoji/family_mwgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png
deleted file mode 100644
index bfefa4879cb..00000000000
--- a/app/assets/images/emoji/family_mwgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png
deleted file mode 100644
index 836feae7c78..00000000000
--- a/app/assets/images/emoji/family_wwb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png
deleted file mode 100644
index 6c6ba45e7bb..00000000000
--- a/app/assets/images/emoji/family_wwbb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png
deleted file mode 100644
index 41225c6fa5a..00000000000
--- a/app/assets/images/emoji/family_wwg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png
deleted file mode 100644
index 284d29ab5da..00000000000
--- a/app/assets/images/emoji/family_wwgb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png
deleted file mode 100644
index d8d3f49b85f..00000000000
--- a/app/assets/images/emoji/family_wwgg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png
deleted file mode 100644
index c406fedfdb1..00000000000
--- a/app/assets/images/emoji/fast_forward.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png
deleted file mode 100644
index 6f929e294c2..00000000000
--- a/app/assets/images/emoji/fax.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png
deleted file mode 100644
index eb8b347cef9..00000000000
--- a/app/assets/images/emoji/fearful.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png
deleted file mode 100644
index 5fe568cee93..00000000000
--- a/app/assets/images/emoji/feet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png
deleted file mode 100644
index 5288c920eb9..00000000000
--- a/app/assets/images/emoji/fencer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png
deleted file mode 100644
index 55c8ff0475b..00000000000
--- a/app/assets/images/emoji/ferris_wheel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png
deleted file mode 100644
index 41816b3ae34..00000000000
--- a/app/assets/images/emoji/ferry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png
deleted file mode 100644
index 839637716ee..00000000000
--- a/app/assets/images/emoji/field_hockey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png
deleted file mode 100644
index fddc65dde96..00000000000
--- a/app/assets/images/emoji/file_cabinet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png
deleted file mode 100644
index addedaf0870..00000000000
--- a/app/assets/images/emoji/file_folder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png
deleted file mode 100644
index 30143aedbe6..00000000000
--- a/app/assets/images/emoji/film_frames.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png
deleted file mode 100644
index 4cd18514ea3..00000000000
--- a/app/assets/images/emoji/fingers_crossed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png
deleted file mode 100644
index dd2384a6cd5..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png
deleted file mode 100644
index 6228401befe..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png
deleted file mode 100644
index b1074da15f5..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png
deleted file mode 100644
index 75e05e4d332..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png
deleted file mode 100644
index 761aebdc30f..00000000000
--- a/app/assets/images/emoji/fingers_crossed_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png
deleted file mode 100644
index bd3775a460b..00000000000
--- a/app/assets/images/emoji/fire.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png
deleted file mode 100644
index 2cd45b7cf7e..00000000000
--- a/app/assets/images/emoji/fire_engine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png
deleted file mode 100644
index 176c8b58265..00000000000
--- a/app/assets/images/emoji/fireworks.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png
deleted file mode 100644
index 15612b66492..00000000000
--- a/app/assets/images/emoji/first_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png
deleted file mode 100644
index 5dccaf72a4f..00000000000
--- a/app/assets/images/emoji/first_quarter_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png
deleted file mode 100644
index cd8a3d7acd8..00000000000
--- a/app/assets/images/emoji/first_quarter_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png
deleted file mode 100644
index c2d2faaacd4..00000000000
--- a/app/assets/images/emoji/fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png
deleted file mode 100644
index 157bded65db..00000000000
--- a/app/assets/images/emoji/fish_cake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png
deleted file mode 100644
index dfcdf07eb50..00000000000
--- a/app/assets/images/emoji/fishing_pole_and_fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png
deleted file mode 100644
index de33592bf98..00000000000
--- a/app/assets/images/emoji/fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png
deleted file mode 100644
index 02809e2dd68..00000000000
--- a/app/assets/images/emoji/fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png
deleted file mode 100644
index 5de34810383..00000000000
--- a/app/assets/images/emoji/fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png
deleted file mode 100644
index 0d5240129b1..00000000000
--- a/app/assets/images/emoji/fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png
deleted file mode 100644
index a95c0dd634b..00000000000
--- a/app/assets/images/emoji/fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png
deleted file mode 100644
index a2f092fd8c7..00000000000
--- a/app/assets/images/emoji/fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png
deleted file mode 100644
index d14371f3f27..00000000000
--- a/app/assets/images/emoji/five.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png
deleted file mode 100644
index 286239920c7..00000000000
--- a/app/assets/images/emoji/flag_ac.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png
deleted file mode 100644
index 20f4b14e8ad..00000000000
--- a/app/assets/images/emoji/flag_ad.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png
deleted file mode 100644
index d16ffe4b862..00000000000
--- a/app/assets/images/emoji/flag_ae.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png
deleted file mode 100644
index a51533b554d..00000000000
--- a/app/assets/images/emoji/flag_af.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png
deleted file mode 100644
index 07f2ce397d0..00000000000
--- a/app/assets/images/emoji/flag_ag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png
deleted file mode 100644
index 500b5ab09fb..00000000000
--- a/app/assets/images/emoji/flag_ai.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png
deleted file mode 100644
index 03a20132cc6..00000000000
--- a/app/assets/images/emoji/flag_al.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png
deleted file mode 100644
index 2ad60a273ec..00000000000
--- a/app/assets/images/emoji/flag_am.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png
deleted file mode 100644
index cb46c31f862..00000000000
--- a/app/assets/images/emoji/flag_ao.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png
deleted file mode 100644
index b272021d375..00000000000
--- a/app/assets/images/emoji/flag_aq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png
deleted file mode 100644
index 73136caf3b7..00000000000
--- a/app/assets/images/emoji/flag_ar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png
deleted file mode 100644
index 3db45a0d9f3..00000000000
--- a/app/assets/images/emoji/flag_as.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png
deleted file mode 100644
index c43769dcb19..00000000000
--- a/app/assets/images/emoji/flag_at.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png
deleted file mode 100644
index 7794309c78c..00000000000
--- a/app/assets/images/emoji/flag_au.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png
deleted file mode 100644
index 02c840d12c9..00000000000
--- a/app/assets/images/emoji/flag_aw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png
deleted file mode 100644
index fc5466174bb..00000000000
--- a/app/assets/images/emoji/flag_ax.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png
deleted file mode 100644
index 89d3d15fd9f..00000000000
--- a/app/assets/images/emoji/flag_az.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png
deleted file mode 100644
index 25fe407e13c..00000000000
--- a/app/assets/images/emoji/flag_ba.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png
deleted file mode 100644
index bccd8c5c9b0..00000000000
--- a/app/assets/images/emoji/flag_bb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png
deleted file mode 100644
index b0597a3149b..00000000000
--- a/app/assets/images/emoji/flag_bd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png
deleted file mode 100644
index 551f086e3c4..00000000000
--- a/app/assets/images/emoji/flag_be.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png
deleted file mode 100644
index 444d4829f94..00000000000
--- a/app/assets/images/emoji/flag_bf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png
deleted file mode 100644
index 821eee5e170..00000000000
--- a/app/assets/images/emoji/flag_bg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png
deleted file mode 100644
index f33724249f0..00000000000
--- a/app/assets/images/emoji/flag_bh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png
deleted file mode 100644
index ea20ac93211..00000000000
--- a/app/assets/images/emoji/flag_bi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png
deleted file mode 100644
index 7cca4f80457..00000000000
--- a/app/assets/images/emoji/flag_bj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png
deleted file mode 100644
index 1082e78999f..00000000000
--- a/app/assets/images/emoji/flag_bl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png
deleted file mode 100644
index 0e28d05d5ac..00000000000
--- a/app/assets/images/emoji/flag_black.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png
deleted file mode 100644
index ab8cafdac63..00000000000
--- a/app/assets/images/emoji/flag_bm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png
deleted file mode 100644
index caa9329a896..00000000000
--- a/app/assets/images/emoji/flag_bn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png
deleted file mode 100644
index 98af62b3da7..00000000000
--- a/app/assets/images/emoji/flag_bo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png
deleted file mode 100644
index cb978ef9de9..00000000000
--- a/app/assets/images/emoji/flag_bq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png
deleted file mode 100644
index b139366a42b..00000000000
--- a/app/assets/images/emoji/flag_br.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png
deleted file mode 100644
index d36bcd2fb52..00000000000
--- a/app/assets/images/emoji/flag_bs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png
deleted file mode 100644
index ed57aa0360e..00000000000
--- a/app/assets/images/emoji/flag_bt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png
deleted file mode 100644
index 5884e648228..00000000000
--- a/app/assets/images/emoji/flag_bv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png
deleted file mode 100644
index cb12f34739d..00000000000
--- a/app/assets/images/emoji/flag_bw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png
deleted file mode 100644
index 859c05beb13..00000000000
--- a/app/assets/images/emoji/flag_by.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png
deleted file mode 100644
index 34761cd03d8..00000000000
--- a/app/assets/images/emoji/flag_bz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png
deleted file mode 100644
index 7c5b390e85b..00000000000
--- a/app/assets/images/emoji/flag_ca.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png
deleted file mode 100644
index b6555a23d83..00000000000
--- a/app/assets/images/emoji/flag_cc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png
deleted file mode 100644
index fa92009771d..00000000000
--- a/app/assets/images/emoji/flag_cd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png
deleted file mode 100644
index b969ae29ea9..00000000000
--- a/app/assets/images/emoji/flag_cf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png
deleted file mode 100644
index 3a38a40a95e..00000000000
--- a/app/assets/images/emoji/flag_cg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png
deleted file mode 100644
index 5ff86b8a3b7..00000000000
--- a/app/assets/images/emoji/flag_ch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png
deleted file mode 100644
index e3b4d15c7f1..00000000000
--- a/app/assets/images/emoji/flag_ci.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png
deleted file mode 100644
index b6b53dbc1c4..00000000000
--- a/app/assets/images/emoji/flag_ck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png
deleted file mode 100644
index c9390da5499..00000000000
--- a/app/assets/images/emoji/flag_cl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png
deleted file mode 100644
index 2d3f6ec4518..00000000000
--- a/app/assets/images/emoji/flag_cm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png
deleted file mode 100644
index 0a7f350a6d2..00000000000
--- a/app/assets/images/emoji/flag_cn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png
deleted file mode 100644
index 7e0f5e0dc3c..00000000000
--- a/app/assets/images/emoji/flag_co.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_cp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png
deleted file mode 100644
index a5fce126515..00000000000
--- a/app/assets/images/emoji/flag_cr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png
deleted file mode 100644
index 447328f7dfd..00000000000
--- a/app/assets/images/emoji/flag_cu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png
deleted file mode 100644
index 43faf4d64d5..00000000000
--- a/app/assets/images/emoji/flag_cv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png
deleted file mode 100644
index eb39e8d0078..00000000000
--- a/app/assets/images/emoji/flag_cw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png
deleted file mode 100644
index 09d21359f3a..00000000000
--- a/app/assets/images/emoji/flag_cx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png
deleted file mode 100644
index 154a7aa3176..00000000000
--- a/app/assets/images/emoji/flag_cy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png
deleted file mode 100644
index 9737ca223c7..00000000000
--- a/app/assets/images/emoji/flag_cz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png
deleted file mode 100644
index 98ed76b3bab..00000000000
--- a/app/assets/images/emoji/flag_de.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png
deleted file mode 100644
index aae927d14b8..00000000000
--- a/app/assets/images/emoji/flag_dg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png
deleted file mode 100644
index 73c2a2acbd9..00000000000
--- a/app/assets/images/emoji/flag_dj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png
deleted file mode 100644
index e5a60b06256..00000000000
--- a/app/assets/images/emoji/flag_dk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png
deleted file mode 100644
index 50f8a53981d..00000000000
--- a/app/assets/images/emoji/flag_dm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png
deleted file mode 100644
index 037a45d7c26..00000000000
--- a/app/assets/images/emoji/flag_do.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png
deleted file mode 100644
index 24945b10f2d..00000000000
--- a/app/assets/images/emoji/flag_dz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png
deleted file mode 100644
index 356ff347838..00000000000
--- a/app/assets/images/emoji/flag_ea.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png
deleted file mode 100644
index 13814594619..00000000000
--- a/app/assets/images/emoji/flag_ec.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png
deleted file mode 100644
index 84f317e7747..00000000000
--- a/app/assets/images/emoji/flag_ee.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png
deleted file mode 100644
index 57786064a95..00000000000
--- a/app/assets/images/emoji/flag_eg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png
deleted file mode 100644
index 4d7a76687f6..00000000000
--- a/app/assets/images/emoji/flag_eh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png
deleted file mode 100644
index 0c3c724c1fb..00000000000
--- a/app/assets/images/emoji/flag_er.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png
deleted file mode 100644
index 3e73597a225..00000000000
--- a/app/assets/images/emoji/flag_es.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png
deleted file mode 100644
index 9560a134c97..00000000000
--- a/app/assets/images/emoji/flag_et.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png
deleted file mode 100644
index 0b456cf3330..00000000000
--- a/app/assets/images/emoji/flag_eu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png
deleted file mode 100644
index ebcf58abfc5..00000000000
--- a/app/assets/images/emoji/flag_fi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png
deleted file mode 100644
index 9cc8c37fe37..00000000000
--- a/app/assets/images/emoji/flag_fj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png
deleted file mode 100644
index 61372fd2549..00000000000
--- a/app/assets/images/emoji/flag_fk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png
deleted file mode 100644
index 0889825c8e1..00000000000
--- a/app/assets/images/emoji/flag_fm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png
deleted file mode 100644
index 9a4431b0831..00000000000
--- a/app/assets/images/emoji/flag_fo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png
deleted file mode 100644
index 62ca19c3fcf..00000000000
--- a/app/assets/images/emoji/flag_fr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png
deleted file mode 100644
index 2e68e527a3e..00000000000
--- a/app/assets/images/emoji/flag_ga.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png
deleted file mode 100644
index 3ed10f62347..00000000000
--- a/app/assets/images/emoji/flag_gb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png
deleted file mode 100644
index 527aad33807..00000000000
--- a/app/assets/images/emoji/flag_gd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png
deleted file mode 100644
index a75d142480d..00000000000
--- a/app/assets/images/emoji/flag_ge.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png
deleted file mode 100644
index 0cf96f327c0..00000000000
--- a/app/assets/images/emoji/flag_gf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png
deleted file mode 100644
index 970002c7f76..00000000000
--- a/app/assets/images/emoji/flag_gg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png
deleted file mode 100644
index f31b5eb7b45..00000000000
--- a/app/assets/images/emoji/flag_gh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png
deleted file mode 100644
index e554a2a1d0c..00000000000
--- a/app/assets/images/emoji/flag_gi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png
deleted file mode 100644
index 2e795dd4e33..00000000000
--- a/app/assets/images/emoji/flag_gl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png
deleted file mode 100644
index bb69c0975a3..00000000000
--- a/app/assets/images/emoji/flag_gm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png
deleted file mode 100644
index 1981f61dbf5..00000000000
--- a/app/assets/images/emoji/flag_gn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png
deleted file mode 100644
index 10e42e672bd..00000000000
--- a/app/assets/images/emoji/flag_gp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png
deleted file mode 100644
index 11475e61eeb..00000000000
--- a/app/assets/images/emoji/flag_gq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png
deleted file mode 100644
index 0f6bb1b6b94..00000000000
--- a/app/assets/images/emoji/flag_gr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png
deleted file mode 100644
index 6fc92780453..00000000000
--- a/app/assets/images/emoji/flag_gs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png
deleted file mode 100644
index 7213d4139ed..00000000000
--- a/app/assets/images/emoji/flag_gt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png
deleted file mode 100644
index 4027549ca3c..00000000000
--- a/app/assets/images/emoji/flag_gu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png
deleted file mode 100644
index 6357f6225f4..00000000000
--- a/app/assets/images/emoji/flag_gw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png
deleted file mode 100644
index 746e2fb7e44..00000000000
--- a/app/assets/images/emoji/flag_gy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png
deleted file mode 100644
index cf0c7151b56..00000000000
--- a/app/assets/images/emoji/flag_hk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png
deleted file mode 100644
index b613509e466..00000000000
--- a/app/assets/images/emoji/flag_hm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png
deleted file mode 100644
index 402cdcefdf8..00000000000
--- a/app/assets/images/emoji/flag_hn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png
deleted file mode 100644
index 46f4f06b4f2..00000000000
--- a/app/assets/images/emoji/flag_hr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png
deleted file mode 100644
index d8d0c888498..00000000000
--- a/app/assets/images/emoji/flag_ht.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png
deleted file mode 100644
index a898de636a5..00000000000
--- a/app/assets/images/emoji/flag_hu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png
deleted file mode 100644
index 69fd990aa95..00000000000
--- a/app/assets/images/emoji/flag_ic.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png
deleted file mode 100644
index 85b4c063a45..00000000000
--- a/app/assets/images/emoji/flag_id.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png
deleted file mode 100644
index a28295838cc..00000000000
--- a/app/assets/images/emoji/flag_ie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png
deleted file mode 100644
index 85c410d45fb..00000000000
--- a/app/assets/images/emoji/flag_il.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png
deleted file mode 100644
index 60a2458e38e..00000000000
--- a/app/assets/images/emoji/flag_im.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png
deleted file mode 100644
index feccc8952ce..00000000000
--- a/app/assets/images/emoji/flag_in.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png
deleted file mode 100644
index aae927d14b8..00000000000
--- a/app/assets/images/emoji/flag_io.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png
deleted file mode 100644
index 41fd1db6f86..00000000000
--- a/app/assets/images/emoji/flag_iq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png
deleted file mode 100644
index ff7aaf62ba6..00000000000
--- a/app/assets/images/emoji/flag_ir.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png
deleted file mode 100644
index ad8d4131dd2..00000000000
--- a/app/assets/images/emoji/flag_is.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png
deleted file mode 100644
index f21563ec533..00000000000
--- a/app/assets/images/emoji/flag_it.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png
deleted file mode 100644
index 198a918f6a4..00000000000
--- a/app/assets/images/emoji/flag_je.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png
deleted file mode 100644
index f84e4f9e8db..00000000000
--- a/app/assets/images/emoji/flag_jm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png
deleted file mode 100644
index 20bfa147e3e..00000000000
--- a/app/assets/images/emoji/flag_jo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png
deleted file mode 100644
index 8d8838e4708..00000000000
--- a/app/assets/images/emoji/flag_jp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png
deleted file mode 100644
index 9e417ab3009..00000000000
--- a/app/assets/images/emoji/flag_ke.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png
deleted file mode 100644
index 2f2d848fe58..00000000000
--- a/app/assets/images/emoji/flag_kg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png
deleted file mode 100644
index 9a2877dd620..00000000000
--- a/app/assets/images/emoji/flag_kh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png
deleted file mode 100644
index 10e507e3245..00000000000
--- a/app/assets/images/emoji/flag_ki.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png
deleted file mode 100644
index bd5a0588e03..00000000000
--- a/app/assets/images/emoji/flag_km.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png
deleted file mode 100644
index 776207c9605..00000000000
--- a/app/assets/images/emoji/flag_kn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png
deleted file mode 100644
index 6b3fd89eaaa..00000000000
--- a/app/assets/images/emoji/flag_kp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png
deleted file mode 100644
index 833a88116e1..00000000000
--- a/app/assets/images/emoji/flag_kr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png
deleted file mode 100644
index 4d19bfa6ca7..00000000000
--- a/app/assets/images/emoji/flag_kw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png
deleted file mode 100644
index 40daa4da597..00000000000
--- a/app/assets/images/emoji/flag_ky.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png
deleted file mode 100644
index 2f97a8fd3c6..00000000000
--- a/app/assets/images/emoji/flag_kz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png
deleted file mode 100644
index 4d4179f34f6..00000000000
--- a/app/assets/images/emoji/flag_la.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png
deleted file mode 100644
index 3d594467011..00000000000
--- a/app/assets/images/emoji/flag_lb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png
deleted file mode 100644
index 45547b1e439..00000000000
--- a/app/assets/images/emoji/flag_lc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png
deleted file mode 100644
index 0eafa6a2215..00000000000
--- a/app/assets/images/emoji/flag_li.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png
deleted file mode 100644
index ab4fe10c40c..00000000000
--- a/app/assets/images/emoji/flag_lk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png
deleted file mode 100644
index f66f267fea2..00000000000
--- a/app/assets/images/emoji/flag_lr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png
deleted file mode 100644
index 24745631e3c..00000000000
--- a/app/assets/images/emoji/flag_ls.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png
deleted file mode 100644
index d644b56d62a..00000000000
--- a/app/assets/images/emoji/flag_lt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png
deleted file mode 100644
index a2df9c92994..00000000000
--- a/app/assets/images/emoji/flag_lu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png
deleted file mode 100644
index ae680d5f0e3..00000000000
--- a/app/assets/images/emoji/flag_lv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png
deleted file mode 100644
index f6e77b0f3ba..00000000000
--- a/app/assets/images/emoji/flag_ly.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png
deleted file mode 100644
index c4a056722cd..00000000000
--- a/app/assets/images/emoji/flag_ma.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png
deleted file mode 100644
index d479eab98cb..00000000000
--- a/app/assets/images/emoji/flag_mc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png
deleted file mode 100644
index a7a72539872..00000000000
--- a/app/assets/images/emoji/flag_md.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png
deleted file mode 100644
index 7c771e7e120..00000000000
--- a/app/assets/images/emoji/flag_me.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_mf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png
deleted file mode 100644
index 2f3ccdda76f..00000000000
--- a/app/assets/images/emoji/flag_mg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png
deleted file mode 100644
index 598016481c1..00000000000
--- a/app/assets/images/emoji/flag_mh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png
deleted file mode 100644
index 7ba775ee75c..00000000000
--- a/app/assets/images/emoji/flag_mk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png
deleted file mode 100644
index 68343785468..00000000000
--- a/app/assets/images/emoji/flag_ml.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png
deleted file mode 100644
index 37dc7d71591..00000000000
--- a/app/assets/images/emoji/flag_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png
deleted file mode 100644
index 1f146bbcd1a..00000000000
--- a/app/assets/images/emoji/flag_mn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png
deleted file mode 100644
index 7edde31f64b..00000000000
--- a/app/assets/images/emoji/flag_mo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png
deleted file mode 100644
index 17ec1c441ed..00000000000
--- a/app/assets/images/emoji/flag_mp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png
deleted file mode 100644
index 1e672dc9087..00000000000
--- a/app/assets/images/emoji/flag_mq.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png
deleted file mode 100644
index f87de46effe..00000000000
--- a/app/assets/images/emoji/flag_mr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png
deleted file mode 100644
index 480b0d4ebda..00000000000
--- a/app/assets/images/emoji/flag_ms.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png
deleted file mode 100644
index c9e1dbdce82..00000000000
--- a/app/assets/images/emoji/flag_mt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png
deleted file mode 100644
index 55b33cb7c33..00000000000
--- a/app/assets/images/emoji/flag_mu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png
deleted file mode 100644
index ce5867126ae..00000000000
--- a/app/assets/images/emoji/flag_mv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png
deleted file mode 100644
index 003d8548401..00000000000
--- a/app/assets/images/emoji/flag_mw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png
deleted file mode 100644
index 42572bcd0ba..00000000000
--- a/app/assets/images/emoji/flag_mx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png
deleted file mode 100644
index 17526c26742..00000000000
--- a/app/assets/images/emoji/flag_my.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png
deleted file mode 100644
index 2352a78e786..00000000000
--- a/app/assets/images/emoji/flag_mz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png
deleted file mode 100644
index ed31c3df04d..00000000000
--- a/app/assets/images/emoji/flag_na.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png
deleted file mode 100644
index 90b3afebfa3..00000000000
--- a/app/assets/images/emoji/flag_nc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png
deleted file mode 100644
index f98a1173c2a..00000000000
--- a/app/assets/images/emoji/flag_ne.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png
deleted file mode 100644
index 9099e767420..00000000000
--- a/app/assets/images/emoji/flag_nf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png
deleted file mode 100644
index ea0abeff1a1..00000000000
--- a/app/assets/images/emoji/flag_ng.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png
deleted file mode 100644
index 772920dfa10..00000000000
--- a/app/assets/images/emoji/flag_ni.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png
deleted file mode 100644
index 83a0e817e41..00000000000
--- a/app/assets/images/emoji/flag_nl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png
deleted file mode 100644
index 99d3142eb7b..00000000000
--- a/app/assets/images/emoji/flag_no.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png
deleted file mode 100644
index 87425a8dfef..00000000000
--- a/app/assets/images/emoji/flag_np.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png
deleted file mode 100644
index b3e3a5d5621..00000000000
--- a/app/assets/images/emoji/flag_nr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png
deleted file mode 100644
index f03614443ee..00000000000
--- a/app/assets/images/emoji/flag_nu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png
deleted file mode 100644
index a4eeeab9cd9..00000000000
--- a/app/assets/images/emoji/flag_nz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png
deleted file mode 100644
index ea824ba31e7..00000000000
--- a/app/assets/images/emoji/flag_om.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png
deleted file mode 100644
index c3091d89889..00000000000
--- a/app/assets/images/emoji/flag_pa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png
deleted file mode 100644
index 39223aa9dbb..00000000000
--- a/app/assets/images/emoji/flag_pe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png
deleted file mode 100644
index 113445f8f6e..00000000000
--- a/app/assets/images/emoji/flag_pf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png
deleted file mode 100644
index 825e9dcb762..00000000000
--- a/app/assets/images/emoji/flag_pg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png
deleted file mode 100644
index 8260e15bd2c..00000000000
--- a/app/assets/images/emoji/flag_ph.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png
deleted file mode 100644
index a7b6a1c5074..00000000000
--- a/app/assets/images/emoji/flag_pk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png
deleted file mode 100644
index 19de2edec11..00000000000
--- a/app/assets/images/emoji/flag_pl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png
deleted file mode 100644
index 2ca60554193..00000000000
--- a/app/assets/images/emoji/flag_pm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png
deleted file mode 100644
index f2263b154bc..00000000000
--- a/app/assets/images/emoji/flag_pn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png
deleted file mode 100644
index d0209cddb79..00000000000
--- a/app/assets/images/emoji/flag_pr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png
deleted file mode 100644
index 7ccab09778b..00000000000
--- a/app/assets/images/emoji/flag_ps.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png
deleted file mode 100644
index cc93f27c64b..00000000000
--- a/app/assets/images/emoji/flag_pt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png
deleted file mode 100644
index 154b2f12d3c..00000000000
--- a/app/assets/images/emoji/flag_pw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png
deleted file mode 100644
index 662ad2f6ff1..00000000000
--- a/app/assets/images/emoji/flag_py.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png
deleted file mode 100644
index a01d8b05cc7..00000000000
--- a/app/assets/images/emoji/flag_qa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png
deleted file mode 100644
index 57f2bbe9df8..00000000000
--- a/app/assets/images/emoji/flag_re.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png
deleted file mode 100644
index 3e48c447706..00000000000
--- a/app/assets/images/emoji/flag_ro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png
deleted file mode 100644
index 9df6c9a5235..00000000000
--- a/app/assets/images/emoji/flag_rs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png
deleted file mode 100644
index e50c9db90e7..00000000000
--- a/app/assets/images/emoji/flag_ru.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png
deleted file mode 100644
index c238c874e1d..00000000000
--- a/app/assets/images/emoji/flag_rw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png
deleted file mode 100644
index 4941be7d198..00000000000
--- a/app/assets/images/emoji/flag_sa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png
deleted file mode 100644
index 7d8f1ac6130..00000000000
--- a/app/assets/images/emoji/flag_sb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png
deleted file mode 100644
index 6ae4d90765e..00000000000
--- a/app/assets/images/emoji/flag_sc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png
deleted file mode 100644
index 963be1b36fb..00000000000
--- a/app/assets/images/emoji/flag_sd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png
deleted file mode 100644
index fc0d0e0ce89..00000000000
--- a/app/assets/images/emoji/flag_se.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png
deleted file mode 100644
index de3c7737c42..00000000000
--- a/app/assets/images/emoji/flag_sg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png
deleted file mode 100644
index 40cd9e44e96..00000000000
--- a/app/assets/images/emoji/flag_sh.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png
deleted file mode 100644
index e308999dba2..00000000000
--- a/app/assets/images/emoji/flag_si.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png
deleted file mode 100644
index 5884e648228..00000000000
--- a/app/assets/images/emoji/flag_sj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png
deleted file mode 100644
index 4259d0e1418..00000000000
--- a/app/assets/images/emoji/flag_sk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png
deleted file mode 100644
index d2cc68830ab..00000000000
--- a/app/assets/images/emoji/flag_sl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png
deleted file mode 100644
index 03b8708754e..00000000000
--- a/app/assets/images/emoji/flag_sm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png
deleted file mode 100644
index 5368bbe93df..00000000000
--- a/app/assets/images/emoji/flag_sn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png
deleted file mode 100644
index 68a0597365a..00000000000
--- a/app/assets/images/emoji/flag_so.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png
deleted file mode 100644
index d3251327035..00000000000
--- a/app/assets/images/emoji/flag_sr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png
deleted file mode 100644
index 122977e798f..00000000000
--- a/app/assets/images/emoji/flag_ss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png
deleted file mode 100644
index f83a863d612..00000000000
--- a/app/assets/images/emoji/flag_st.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png
deleted file mode 100644
index efb83e2f253..00000000000
--- a/app/assets/images/emoji/flag_sv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png
deleted file mode 100644
index 94b760fbedf..00000000000
--- a/app/assets/images/emoji/flag_sx.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png
deleted file mode 100644
index 09a8ee8f78c..00000000000
--- a/app/assets/images/emoji/flag_sy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png
deleted file mode 100644
index f74e82ea1fd..00000000000
--- a/app/assets/images/emoji/flag_sz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png
deleted file mode 100644
index b44283e90e2..00000000000
--- a/app/assets/images/emoji/flag_ta.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png
deleted file mode 100644
index 156b33d1ba6..00000000000
--- a/app/assets/images/emoji/flag_tc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png
deleted file mode 100644
index ebe7f592828..00000000000
--- a/app/assets/images/emoji/flag_td.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png
deleted file mode 100644
index a1a3ad68ee2..00000000000
--- a/app/assets/images/emoji/flag_tf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png
deleted file mode 100644
index 826b73c9ac5..00000000000
--- a/app/assets/images/emoji/flag_tg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png
deleted file mode 100644
index 93ff542c5a6..00000000000
--- a/app/assets/images/emoji/flag_th.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png
deleted file mode 100644
index 7a8a0b6190a..00000000000
--- a/app/assets/images/emoji/flag_tj.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png
deleted file mode 100644
index 2fa5a21b1bb..00000000000
--- a/app/assets/images/emoji/flag_tk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png
deleted file mode 100644
index 5b120eccc6f..00000000000
--- a/app/assets/images/emoji/flag_tl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png
deleted file mode 100644
index c3c4f532302..00000000000
--- a/app/assets/images/emoji/flag_tm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png
deleted file mode 100644
index 58ef161229f..00000000000
--- a/app/assets/images/emoji/flag_tn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png
deleted file mode 100644
index 1ffa7bb9d19..00000000000
--- a/app/assets/images/emoji/flag_to.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png
deleted file mode 100644
index 325251fae88..00000000000
--- a/app/assets/images/emoji/flag_tr.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png
deleted file mode 100644
index ed3bb39a300..00000000000
--- a/app/assets/images/emoji/flag_tt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png
deleted file mode 100644
index e82c65c7bb9..00000000000
--- a/app/assets/images/emoji/flag_tv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png
deleted file mode 100644
index 3a8f00b5928..00000000000
--- a/app/assets/images/emoji/flag_tw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png
deleted file mode 100644
index 2a020853d4e..00000000000
--- a/app/assets/images/emoji/flag_tz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png
deleted file mode 100644
index cd84d1bbd36..00000000000
--- a/app/assets/images/emoji/flag_ua.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png
deleted file mode 100644
index dc97690eb55..00000000000
--- a/app/assets/images/emoji/flag_ug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png
deleted file mode 100644
index 4a7ee3cdf13..00000000000
--- a/app/assets/images/emoji/flag_um.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png
deleted file mode 100644
index 9f730305860..00000000000
--- a/app/assets/images/emoji/flag_us.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png
deleted file mode 100644
index b8002a697a6..00000000000
--- a/app/assets/images/emoji/flag_uy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png
deleted file mode 100644
index d56ca9bc424..00000000000
--- a/app/assets/images/emoji/flag_uz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png
deleted file mode 100644
index ddaf5e3141b..00000000000
--- a/app/assets/images/emoji/flag_va.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png
deleted file mode 100644
index 43703c62a71..00000000000
--- a/app/assets/images/emoji/flag_vc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png
deleted file mode 100644
index 1b62796824e..00000000000
--- a/app/assets/images/emoji/flag_ve.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png
deleted file mode 100644
index 536f780f1c0..00000000000
--- a/app/assets/images/emoji/flag_vg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png
deleted file mode 100644
index 64102012cfe..00000000000
--- a/app/assets/images/emoji/flag_vi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png
deleted file mode 100644
index 427036046b6..00000000000
--- a/app/assets/images/emoji/flag_vn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png
deleted file mode 100644
index 706eba44070..00000000000
--- a/app/assets/images/emoji/flag_vu.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png
deleted file mode 100644
index 70c761036bd..00000000000
--- a/app/assets/images/emoji/flag_wf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png
deleted file mode 100644
index 86d6e96d5e9..00000000000
--- a/app/assets/images/emoji/flag_white.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png
deleted file mode 100644
index a1ea0703141..00000000000
--- a/app/assets/images/emoji/flag_ws.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png
deleted file mode 100644
index e587a446632..00000000000
--- a/app/assets/images/emoji/flag_xk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png
deleted file mode 100644
index eadfebd5f67..00000000000
--- a/app/assets/images/emoji/flag_ye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png
deleted file mode 100644
index c81fa6d886e..00000000000
--- a/app/assets/images/emoji/flag_yt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png
deleted file mode 100644
index f397ef5072f..00000000000
--- a/app/assets/images/emoji/flag_za.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png
deleted file mode 100644
index 2494a31f662..00000000000
--- a/app/assets/images/emoji/flag_zm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png
deleted file mode 100644
index e09b9652be6..00000000000
--- a/app/assets/images/emoji/flag_zw.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png
deleted file mode 100644
index 3b451035a3a..00000000000
--- a/app/assets/images/emoji/flags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png
deleted file mode 100644
index eee36c25067..00000000000
--- a/app/assets/images/emoji/flashlight.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png
deleted file mode 100644
index c9250d27fa7..00000000000
--- a/app/assets/images/emoji/fleur-de-lis.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png
deleted file mode 100644
index 072a76d3c13..00000000000
--- a/app/assets/images/emoji/floppy_disk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png
deleted file mode 100644
index 6766b044d95..00000000000
--- a/app/assets/images/emoji/flower_playing_cards.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png
deleted file mode 100644
index 829220bc470..00000000000
--- a/app/assets/images/emoji/flushed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png
deleted file mode 100644
index 4e73c2de272..00000000000
--- a/app/assets/images/emoji/fog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png
deleted file mode 100644
index 57702d8d3ac..00000000000
--- a/app/assets/images/emoji/foggy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png
deleted file mode 100644
index 10366f41fce..00000000000
--- a/app/assets/images/emoji/football.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png
deleted file mode 100644
index b2673c5a1a8..00000000000
--- a/app/assets/images/emoji/footprints.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png
deleted file mode 100644
index 09f1feaea1c..00000000000
--- a/app/assets/images/emoji/fork_and_knife.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png
deleted file mode 100644
index 7411755f708..00000000000
--- a/app/assets/images/emoji/fork_knife_plate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png
deleted file mode 100644
index 293f5d91c0f..00000000000
--- a/app/assets/images/emoji/fountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png
deleted file mode 100644
index b0e914aac45..00000000000
--- a/app/assets/images/emoji/four.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png
deleted file mode 100644
index fdedfcc2b4e..00000000000
--- a/app/assets/images/emoji/four_leaf_clover.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png
deleted file mode 100644
index 1ab339bf054..00000000000
--- a/app/assets/images/emoji/fox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png
deleted file mode 100644
index 9fe84607bfd..00000000000
--- a/app/assets/images/emoji/frame_photo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png
deleted file mode 100644
index b71956eb48a..00000000000
--- a/app/assets/images/emoji/free.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png
deleted file mode 100644
index 4c2c5639822..00000000000
--- a/app/assets/images/emoji/french_bread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png
deleted file mode 100644
index 752ba7f1398..00000000000
--- a/app/assets/images/emoji/fried_shrimp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png
deleted file mode 100644
index 4e2a4caacef..00000000000
--- a/app/assets/images/emoji/fries.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png
deleted file mode 100644
index 8825d1ad577..00000000000
--- a/app/assets/images/emoji/frog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png
deleted file mode 100644
index 43ab6b0a1c1..00000000000
--- a/app/assets/images/emoji/frowning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png
deleted file mode 100644
index 6ae71f233b9..00000000000
--- a/app/assets/images/emoji/frowning2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png
deleted file mode 100644
index 05b18794474..00000000000
--- a/app/assets/images/emoji/fuelpump.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png
deleted file mode 100644
index c9a2d6aa7c9..00000000000
--- a/app/assets/images/emoji/full_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png
deleted file mode 100644
index a5c25bbaf64..00000000000
--- a/app/assets/images/emoji/full_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png
deleted file mode 100644
index ad3626fe5e5..00000000000
--- a/app/assets/images/emoji/game_die.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gay_pride_flag.png b/app/assets/images/emoji/gay_pride_flag.png
deleted file mode 100644
index 1bec5f2ffd7..00000000000
--- a/app/assets/images/emoji/gay_pride_flag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png
deleted file mode 100644
index 2a1cc2c0ff4..00000000000
--- a/app/assets/images/emoji/gear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png
deleted file mode 100644
index db122d26a19..00000000000
--- a/app/assets/images/emoji/gem.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png
deleted file mode 100644
index 1a09698cf00..00000000000
--- a/app/assets/images/emoji/gemini.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png
deleted file mode 100644
index 5650bc0ed18..00000000000
--- a/app/assets/images/emoji/ghost.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png
deleted file mode 100644
index 844e2164560..00000000000
--- a/app/assets/images/emoji/gift.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png
deleted file mode 100644
index 902ceafe4d1..00000000000
--- a/app/assets/images/emoji/gift_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png
deleted file mode 100644
index dc1d4d08b39..00000000000
--- a/app/assets/images/emoji/girl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png
deleted file mode 100644
index bb667e88651..00000000000
--- a/app/assets/images/emoji/girl_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png
deleted file mode 100644
index a59ed4a3f0d..00000000000
--- a/app/assets/images/emoji/girl_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png
deleted file mode 100644
index 517e7f2a7b0..00000000000
--- a/app/assets/images/emoji/girl_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png
deleted file mode 100644
index 542d96c8487..00000000000
--- a/app/assets/images/emoji/girl_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png
deleted file mode 100644
index 66b7c28c2df..00000000000
--- a/app/assets/images/emoji/girl_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png
deleted file mode 100644
index 82450c1a4ba..00000000000
--- a/app/assets/images/emoji/globe_with_meridians.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png
deleted file mode 100644
index df3a53da0fb..00000000000
--- a/app/assets/images/emoji/goal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png
deleted file mode 100644
index f9d9e38a128..00000000000
--- a/app/assets/images/emoji/goat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png
deleted file mode 100644
index f65a21d8a46..00000000000
--- a/app/assets/images/emoji/golf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png
deleted file mode 100644
index 39c552de86d..00000000000
--- a/app/assets/images/emoji/golfer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png
deleted file mode 100644
index acc51e13622..00000000000
--- a/app/assets/images/emoji/gorilla.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png
deleted file mode 100644
index 30d22218896..00000000000
--- a/app/assets/images/emoji/grapes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png
deleted file mode 100644
index 5fd51bd3915..00000000000
--- a/app/assets/images/emoji/green_apple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png
deleted file mode 100644
index e5e411cf3b5..00000000000
--- a/app/assets/images/emoji/green_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png
deleted file mode 100644
index c52d60a58be..00000000000
--- a/app/assets/images/emoji/green_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png
deleted file mode 100644
index 9b64da8bf7f..00000000000
--- a/app/assets/images/emoji/grey_exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png
deleted file mode 100644
index 6e7824c75f6..00000000000
--- a/app/assets/images/emoji/grey_question.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png
deleted file mode 100644
index 871b2f071c9..00000000000
--- a/app/assets/images/emoji/grimacing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png
deleted file mode 100644
index 418d94c811b..00000000000
--- a/app/assets/images/emoji/grin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png
deleted file mode 100644
index 3e8e0dab78c..00000000000
--- a/app/assets/images/emoji/grinning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png
deleted file mode 100644
index 8d7ab3c473c..00000000000
--- a/app/assets/images/emoji/guardsman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png
deleted file mode 100644
index cea9ba27468..00000000000
--- a/app/assets/images/emoji/guardsman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png
deleted file mode 100644
index 037464e4028..00000000000
--- a/app/assets/images/emoji/guardsman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png
deleted file mode 100644
index 0f6726fbe87..00000000000
--- a/app/assets/images/emoji/guardsman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png
deleted file mode 100644
index 85fcf9a3b97..00000000000
--- a/app/assets/images/emoji/guardsman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png
deleted file mode 100644
index e5f9ca7d5a2..00000000000
--- a/app/assets/images/emoji/guardsman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png
deleted file mode 100644
index 43d752f1e3d..00000000000
--- a/app/assets/images/emoji/guitar.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png
deleted file mode 100644
index 89c5c244c7b..00000000000
--- a/app/assets/images/emoji/gun.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png
deleted file mode 100644
index 91266b12930..00000000000
--- a/app/assets/images/emoji/haircut.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png
deleted file mode 100644
index c743b74abeb..00000000000
--- a/app/assets/images/emoji/haircut_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png
deleted file mode 100644
index f144f8e55ce..00000000000
--- a/app/assets/images/emoji/haircut_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png
deleted file mode 100644
index d5ad19563ac..00000000000
--- a/app/assets/images/emoji/haircut_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png
deleted file mode 100644
index 244fd3af008..00000000000
--- a/app/assets/images/emoji/haircut_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png
deleted file mode 100644
index 20a94a88623..00000000000
--- a/app/assets/images/emoji/haircut_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png
deleted file mode 100644
index 3573b28a1fd..00000000000
--- a/app/assets/images/emoji/hamburger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png
deleted file mode 100644
index 00736cce47d..00000000000
--- a/app/assets/images/emoji/hammer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png
deleted file mode 100644
index 3bee30ec588..00000000000
--- a/app/assets/images/emoji/hammer_pick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png
deleted file mode 100644
index 9a04388e4e7..00000000000
--- a/app/assets/images/emoji/hamster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png
deleted file mode 100644
index fb5ae8ebb5a..00000000000
--- a/app/assets/images/emoji/hand_splayed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png
deleted file mode 100644
index a7888e6bd23..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png
deleted file mode 100644
index cc10fbc272d..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png
deleted file mode 100644
index 707236ae8a4..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png
deleted file mode 100644
index 1430df9c61f..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png
deleted file mode 100644
index 80bec971b6b..00000000000
--- a/app/assets/images/emoji/hand_splayed_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png
deleted file mode 100644
index cbf75c5d25e..00000000000
--- a/app/assets/images/emoji/handbag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png
deleted file mode 100644
index 1152f1344c7..00000000000
--- a/app/assets/images/emoji/handball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png
deleted file mode 100644
index c26cac2df98..00000000000
--- a/app/assets/images/emoji/handball_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png
deleted file mode 100644
index 7baaf95a9a2..00000000000
--- a/app/assets/images/emoji/handball_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png
deleted file mode 100644
index 0e3a37c3d40..00000000000
--- a/app/assets/images/emoji/handball_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png
deleted file mode 100644
index e1233f38266..00000000000
--- a/app/assets/images/emoji/handball_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png
deleted file mode 100644
index 6b1eb9b64b0..00000000000
--- a/app/assets/images/emoji/handball_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png
deleted file mode 100644
index c5d35fd8138..00000000000
--- a/app/assets/images/emoji/handshake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png
deleted file mode 100644
index 8f8fbb9bdca..00000000000
--- a/app/assets/images/emoji/handshake_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png
deleted file mode 100644
index 336a77a6d78..00000000000
--- a/app/assets/images/emoji/handshake_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png
deleted file mode 100644
index 95f62d4fecd..00000000000
--- a/app/assets/images/emoji/handshake_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png
deleted file mode 100644
index 2b0a6433886..00000000000
--- a/app/assets/images/emoji/handshake_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png
deleted file mode 100644
index 40189ee68e4..00000000000
--- a/app/assets/images/emoji/handshake_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png
deleted file mode 100644
index 6e26f0070b0..00000000000
--- a/app/assets/images/emoji/hash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png
deleted file mode 100644
index 31dfb511e0e..00000000000
--- a/app/assets/images/emoji/hatched_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png
deleted file mode 100644
index c5b0e8f3bcc..00000000000
--- a/app/assets/images/emoji/hatching_chick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png
deleted file mode 100644
index 0be723085e0..00000000000
--- a/app/assets/images/emoji/head_bandage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png
deleted file mode 100644
index e9fd34041d8..00000000000
--- a/app/assets/images/emoji/headphones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png
deleted file mode 100644
index 74b6be0c6c5..00000000000
--- a/app/assets/images/emoji/hear_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png
deleted file mode 100644
index 638cb72dc4e..00000000000
--- a/app/assets/images/emoji/heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png
deleted file mode 100644
index 5443f60bc63..00000000000
--- a/app/assets/images/emoji/heart_decoration.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png
deleted file mode 100644
index 91b520be40b..00000000000
--- a/app/assets/images/emoji/heart_exclamation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png
deleted file mode 100644
index 73fbee29d4e..00000000000
--- a/app/assets/images/emoji/heart_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png
deleted file mode 100644
index bc5a833f9a1..00000000000
--- a/app/assets/images/emoji/heart_eyes_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png
deleted file mode 100644
index 0bcf2d1d567..00000000000
--- a/app/assets/images/emoji/heartbeat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png
deleted file mode 100644
index d6e694e972f..00000000000
--- a/app/assets/images/emoji/heartpulse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png
deleted file mode 100644
index 393c3ed5267..00000000000
--- a/app/assets/images/emoji/hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png
deleted file mode 100644
index 03bd695377e..00000000000
--- a/app/assets/images/emoji/heavy_check_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png
deleted file mode 100644
index df32ab21bea..00000000000
--- a/app/assets/images/emoji/heavy_division_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png
deleted file mode 100644
index ef2c2e20590..00000000000
--- a/app/assets/images/emoji/heavy_dollar_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png
deleted file mode 100644
index 054211caf12..00000000000
--- a/app/assets/images/emoji/heavy_minus_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png
deleted file mode 100644
index e47cc1b685d..00000000000
--- a/app/assets/images/emoji/heavy_multiplication_x.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png
deleted file mode 100644
index 40799798aaf..00000000000
--- a/app/assets/images/emoji/heavy_plus_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png
deleted file mode 100644
index 7ec5f39a51a..00000000000
--- a/app/assets/images/emoji/helicopter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png
deleted file mode 100644
index 7140a676038..00000000000
--- a/app/assets/images/emoji/helmet_with_cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png
deleted file mode 100644
index d984d1562bb..00000000000
--- a/app/assets/images/emoji/herb.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png
deleted file mode 100644
index 39dd3524233..00000000000
--- a/app/assets/images/emoji/hibiscus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png
deleted file mode 100644
index c41f2d5fd50..00000000000
--- a/app/assets/images/emoji/high_brightness.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png
deleted file mode 100644
index b331cbccc9d..00000000000
--- a/app/assets/images/emoji/high_heel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png
deleted file mode 100644
index be94e9cbf73..00000000000
--- a/app/assets/images/emoji/hockey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png
deleted file mode 100644
index 517d2ae0deb..00000000000
--- a/app/assets/images/emoji/hole.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png
deleted file mode 100644
index 6ab4a2a2651..00000000000
--- a/app/assets/images/emoji/homes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png
deleted file mode 100644
index 9d8f592955e..00000000000
--- a/app/assets/images/emoji/honey_pot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png
deleted file mode 100644
index 7cb1172f4e4..00000000000
--- a/app/assets/images/emoji/horse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png
deleted file mode 100644
index addf9edac56..00000000000
--- a/app/assets/images/emoji/horse_racing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png
deleted file mode 100644
index e9bf4092e98..00000000000
--- a/app/assets/images/emoji/horse_racing_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png
deleted file mode 100644
index 031bbc3d867..00000000000
--- a/app/assets/images/emoji/horse_racing_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png
deleted file mode 100644
index b40ef891f9b..00000000000
--- a/app/assets/images/emoji/horse_racing_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png
deleted file mode 100644
index e286cb85065..00000000000
--- a/app/assets/images/emoji/horse_racing_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png
deleted file mode 100644
index 453c51c6007..00000000000
--- a/app/assets/images/emoji/horse_racing_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png
deleted file mode 100644
index 1cbce4ae767..00000000000
--- a/app/assets/images/emoji/hospital.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png
deleted file mode 100644
index 266675bd577..00000000000
--- a/app/assets/images/emoji/hot_pepper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png
deleted file mode 100644
index 3c3354d94cb..00000000000
--- a/app/assets/images/emoji/hotdog.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png
deleted file mode 100644
index ea8f4c4979a..00000000000
--- a/app/assets/images/emoji/hotel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png
deleted file mode 100644
index 3d9df2d9475..00000000000
--- a/app/assets/images/emoji/hotsprings.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png
deleted file mode 100644
index a5db2d1d3f4..00000000000
--- a/app/assets/images/emoji/hourglass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png
deleted file mode 100644
index b93b15ed6d8..00000000000
--- a/app/assets/images/emoji/hourglass_flowing_sand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png
deleted file mode 100644
index 01c98a0ba92..00000000000
--- a/app/assets/images/emoji/house.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png
deleted file mode 100644
index c55e81de990..00000000000
--- a/app/assets/images/emoji/house_abandoned.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png
deleted file mode 100644
index 0aae41598ef..00000000000
--- a/app/assets/images/emoji/house_with_garden.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png
deleted file mode 100644
index 5bba6dc6d51..00000000000
--- a/app/assets/images/emoji/hugging.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png
deleted file mode 100644
index cad0e23132e..00000000000
--- a/app/assets/images/emoji/hushed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png
deleted file mode 100644
index 94267b9c434..00000000000
--- a/app/assets/images/emoji/ice_cream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png
deleted file mode 100644
index 8c449b0c039..00000000000
--- a/app/assets/images/emoji/ice_skate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png
deleted file mode 100644
index 8f6546e31a5..00000000000
--- a/app/assets/images/emoji/icecream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png
deleted file mode 100644
index 5bf69bf7ba8..00000000000
--- a/app/assets/images/emoji/id.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png
deleted file mode 100644
index 0c0d589caf0..00000000000
--- a/app/assets/images/emoji/ideograph_advantage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png
deleted file mode 100644
index 9f9a9605539..00000000000
--- a/app/assets/images/emoji/imp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png
deleted file mode 100644
index 41a6be2b0ee..00000000000
--- a/app/assets/images/emoji/inbox_tray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png
deleted file mode 100644
index fd22e88182e..00000000000
--- a/app/assets/images/emoji/incoming_envelope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png
deleted file mode 100644
index 55fc6294d25..00000000000
--- a/app/assets/images/emoji/information_desk_person.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png
deleted file mode 100644
index 3d9e2247940..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png
deleted file mode 100644
index 879e8b7966d..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png
deleted file mode 100644
index 307514eab67..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png
deleted file mode 100644
index 297395dcb3f..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png
deleted file mode 100644
index 26f8f22b28b..00000000000
--- a/app/assets/images/emoji/information_desk_person_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png
deleted file mode 100644
index 871f2db9314..00000000000
--- a/app/assets/images/emoji/information_source.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png
deleted file mode 100644
index 57f5151124f..00000000000
--- a/app/assets/images/emoji/innocent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png
deleted file mode 100644
index 509813e9bb2..00000000000
--- a/app/assets/images/emoji/interrobang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png
deleted file mode 100644
index fd377acf872..00000000000
--- a/app/assets/images/emoji/iphone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png
deleted file mode 100644
index 7fd834389b7..00000000000
--- a/app/assets/images/emoji/island.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png
deleted file mode 100644
index dfd933f6f36..00000000000
--- a/app/assets/images/emoji/izakaya_lantern.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png
deleted file mode 100644
index 44c3fc0aec9..00000000000
--- a/app/assets/images/emoji/jack_o_lantern.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png
deleted file mode 100644
index d86d0a59e12..00000000000
--- a/app/assets/images/emoji/japan.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png
deleted file mode 100644
index 64b4e33a1ae..00000000000
--- a/app/assets/images/emoji/japanese_castle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png
deleted file mode 100644
index 515c6a2250e..00000000000
--- a/app/assets/images/emoji/japanese_goblin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png
deleted file mode 100644
index fe8670fdaf1..00000000000
--- a/app/assets/images/emoji/japanese_ogre.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png
deleted file mode 100644
index 2a6869d674c..00000000000
--- a/app/assets/images/emoji/jeans.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png
deleted file mode 100644
index 0ba3b1859d8..00000000000
--- a/app/assets/images/emoji/joy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png
deleted file mode 100644
index aac353179aa..00000000000
--- a/app/assets/images/emoji/joy_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png
deleted file mode 100644
index 1ee1905434e..00000000000
--- a/app/assets/images/emoji/joystick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png
deleted file mode 100644
index a37f6224a42..00000000000
--- a/app/assets/images/emoji/juggling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png
deleted file mode 100644
index c18eda40031..00000000000
--- a/app/assets/images/emoji/juggling_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png
deleted file mode 100644
index de3b7a555b6..00000000000
--- a/app/assets/images/emoji/juggling_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png
deleted file mode 100644
index 74ab6d85458..00000000000
--- a/app/assets/images/emoji/juggling_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png
deleted file mode 100644
index 1c57823203f..00000000000
--- a/app/assets/images/emoji/juggling_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png
deleted file mode 100644
index c343d6ee98a..00000000000
--- a/app/assets/images/emoji/juggling_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png
deleted file mode 100644
index 1778c1138e4..00000000000
--- a/app/assets/images/emoji/kaaba.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png
deleted file mode 100644
index 319cd1b884c..00000000000
--- a/app/assets/images/emoji/key.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png
deleted file mode 100644
index e11d706c6c8..00000000000
--- a/app/assets/images/emoji/key2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png
deleted file mode 100644
index 75027cb9af7..00000000000
--- a/app/assets/images/emoji/keyboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png
deleted file mode 100644
index abe851115d1..00000000000
--- a/app/assets/images/emoji/kimono.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png
deleted file mode 100644
index 85e6dcfc4e8..00000000000
--- a/app/assets/images/emoji/kiss.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png
deleted file mode 100644
index a9a0edae17c..00000000000
--- a/app/assets/images/emoji/kiss_mm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png
deleted file mode 100644
index fdac73cbb1d..00000000000
--- a/app/assets/images/emoji/kiss_ww.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png
deleted file mode 100644
index 39d325fd8e3..00000000000
--- a/app/assets/images/emoji/kissing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png
deleted file mode 100644
index 6e0bcc77540..00000000000
--- a/app/assets/images/emoji/kissing_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png
deleted file mode 100644
index b684d7d4d6c..00000000000
--- a/app/assets/images/emoji/kissing_closed_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png
deleted file mode 100644
index 0ff808fd614..00000000000
--- a/app/assets/images/emoji/kissing_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png
deleted file mode 100644
index e181f17099d..00000000000
--- a/app/assets/images/emoji/kissing_smiling_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png
deleted file mode 100644
index dfbd8258074..00000000000
--- a/app/assets/images/emoji/kiwi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png
deleted file mode 100644
index 1acb9f3077b..00000000000
--- a/app/assets/images/emoji/knife.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png
deleted file mode 100644
index a0aa437a98c..00000000000
--- a/app/assets/images/emoji/koala.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png
deleted file mode 100644
index 6450eb44d90..00000000000
--- a/app/assets/images/emoji/koko.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png
deleted file mode 100644
index d41c9b4f1e1..00000000000
--- a/app/assets/images/emoji/label.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png
deleted file mode 100644
index 84078ef3127..00000000000
--- a/app/assets/images/emoji/large_blue_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png
deleted file mode 100644
index 416a58bd5a8..00000000000
--- a/app/assets/images/emoji/large_blue_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png
deleted file mode 100644
index 73ff0ac36c8..00000000000
--- a/app/assets/images/emoji/large_orange_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png
deleted file mode 100644
index 0842a0dd408..00000000000
--- a/app/assets/images/emoji/last_quarter_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png
deleted file mode 100644
index 94099343c5d..00000000000
--- a/app/assets/images/emoji/last_quarter_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png
deleted file mode 100644
index d94e9505ba1..00000000000
--- a/app/assets/images/emoji/laughing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png
deleted file mode 100644
index 1e43e1af820..00000000000
--- a/app/assets/images/emoji/leaves.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png
deleted file mode 100644
index 13e7561a4bd..00000000000
--- a/app/assets/images/emoji/ledger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png
deleted file mode 100644
index a9d9fd8d59c..00000000000
--- a/app/assets/images/emoji/left_facing_fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png
deleted file mode 100644
index 1262a6b4b69..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png
deleted file mode 100644
index 40bf70b82b2..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png
deleted file mode 100644
index 93f58145111..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png
deleted file mode 100644
index d82b5ec91f0..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png
deleted file mode 100644
index 09ae4cd492b..00000000000
--- a/app/assets/images/emoji/left_facing_fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png
deleted file mode 100644
index 887b23f3f25..00000000000
--- a/app/assets/images/emoji/left_luggage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png
deleted file mode 100644
index 7937f24f2ac..00000000000
--- a/app/assets/images/emoji/left_right_arrow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png
deleted file mode 100644
index ba45c2ad9e9..00000000000
--- a/app/assets/images/emoji/leftwards_arrow_with_hook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png
deleted file mode 100644
index 9a7d95ca220..00000000000
--- a/app/assets/images/emoji/lemon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png
deleted file mode 100644
index 30158d34de9..00000000000
--- a/app/assets/images/emoji/leo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png
deleted file mode 100644
index 8aac3d49448..00000000000
--- a/app/assets/images/emoji/leopard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png
deleted file mode 100644
index 720a3b34119..00000000000
--- a/app/assets/images/emoji/level_slider.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png
deleted file mode 100644
index 3dc315a3d91..00000000000
--- a/app/assets/images/emoji/levitate.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png
deleted file mode 100644
index 8fd133a357c..00000000000
--- a/app/assets/images/emoji/libra.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png
deleted file mode 100644
index afdeaa476af..00000000000
--- a/app/assets/images/emoji/lifter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png
deleted file mode 100644
index febaad123ec..00000000000
--- a/app/assets/images/emoji/lifter_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png
deleted file mode 100644
index 27ae794a18e..00000000000
--- a/app/assets/images/emoji/lifter_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png
deleted file mode 100644
index 45c4c22c709..00000000000
--- a/app/assets/images/emoji/lifter_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png
deleted file mode 100644
index 67dd21d2464..00000000000
--- a/app/assets/images/emoji/lifter_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png
deleted file mode 100644
index fa0152038b6..00000000000
--- a/app/assets/images/emoji/lifter_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png
deleted file mode 100644
index a64829f5078..00000000000
--- a/app/assets/images/emoji/light_rail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png
deleted file mode 100644
index ae20f0f8eec..00000000000
--- a/app/assets/images/emoji/link.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png
deleted file mode 100644
index 5062ab47ecf..00000000000
--- a/app/assets/images/emoji/lion_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png
deleted file mode 100644
index 35f3cc2006f..00000000000
--- a/app/assets/images/emoji/lips.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png
deleted file mode 100644
index 61a0c084c99..00000000000
--- a/app/assets/images/emoji/lipstick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png
deleted file mode 100644
index 8363876050e..00000000000
--- a/app/assets/images/emoji/lizard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png
deleted file mode 100644
index 5a739c46644..00000000000
--- a/app/assets/images/emoji/lock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png
deleted file mode 100644
index 19a07d162fb..00000000000
--- a/app/assets/images/emoji/lock_with_ink_pen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png
deleted file mode 100644
index ad76d7bf916..00000000000
--- a/app/assets/images/emoji/lollipop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png
deleted file mode 100644
index 0b82c8fe315..00000000000
--- a/app/assets/images/emoji/loop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png
deleted file mode 100644
index 8370033a539..00000000000
--- a/app/assets/images/emoji/loud_sound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png
deleted file mode 100644
index 5fd76a95b82..00000000000
--- a/app/assets/images/emoji/loudspeaker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png
deleted file mode 100644
index 5e136be6f8b..00000000000
--- a/app/assets/images/emoji/love_hotel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png
deleted file mode 100644
index 3c3c767e784..00000000000
--- a/app/assets/images/emoji/love_letter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png
deleted file mode 100644
index 543011d3961..00000000000
--- a/app/assets/images/emoji/low_brightness.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png
deleted file mode 100644
index 02827e2628b..00000000000
--- a/app/assets/images/emoji/lying_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png
deleted file mode 100644
index 8a3506fc1d7..00000000000
--- a/app/assets/images/emoji/m.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png
deleted file mode 100644
index 55487156ac6..00000000000
--- a/app/assets/images/emoji/mag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png
deleted file mode 100644
index 0f4b1bca876..00000000000
--- a/app/assets/images/emoji/mag_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png
deleted file mode 100644
index 66fd32025b2..00000000000
--- a/app/assets/images/emoji/mahjong.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png
deleted file mode 100644
index ef5174e40dd..00000000000
--- a/app/assets/images/emoji/mailbox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png
deleted file mode 100644
index ddc705db0d8..00000000000
--- a/app/assets/images/emoji/mailbox_closed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png
deleted file mode 100644
index 5460616a5b1..00000000000
--- a/app/assets/images/emoji/mailbox_with_mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png
deleted file mode 100644
index f9aeee6b15a..00000000000
--- a/app/assets/images/emoji/mailbox_with_no_mail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png
deleted file mode 100644
index 857a02e5146..00000000000
--- a/app/assets/images/emoji/man.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png
deleted file mode 100644
index ccff3bede5a..00000000000
--- a/app/assets/images/emoji/man_dancing.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png
deleted file mode 100644
index e0b9f82d905..00000000000
--- a/app/assets/images/emoji/man_dancing_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png
deleted file mode 100644
index a5beed56e2e..00000000000
--- a/app/assets/images/emoji/man_dancing_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png
deleted file mode 100644
index 2fa20180a6e..00000000000
--- a/app/assets/images/emoji/man_dancing_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png
deleted file mode 100644
index bd3528c83ba..00000000000
--- a/app/assets/images/emoji/man_dancing_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png
deleted file mode 100644
index 41fd4f880c9..00000000000
--- a/app/assets/images/emoji/man_dancing_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png
deleted file mode 100644
index 5f7e9303f89..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png
deleted file mode 100644
index 7b6b3acd99b..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png
deleted file mode 100644
index 7975191b360..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png
deleted file mode 100644
index a2816f600ae..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png
deleted file mode 100644
index ea8291760f9..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png
deleted file mode 100644
index c743e05fc5e..00000000000
--- a/app/assets/images/emoji/man_in_tuxedo_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png
deleted file mode 100644
index bb86e963a80..00000000000
--- a/app/assets/images/emoji/man_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png
deleted file mode 100644
index fdeeaff46f5..00000000000
--- a/app/assets/images/emoji/man_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png
deleted file mode 100644
index 7ae0b5df9cf..00000000000
--- a/app/assets/images/emoji/man_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png
deleted file mode 100644
index db14cde99b8..00000000000
--- a/app/assets/images/emoji/man_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png
deleted file mode 100644
index 7c67a70529c..00000000000
--- a/app/assets/images/emoji/man_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png
deleted file mode 100644
index 7841e13608d..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 5b7b3def19c..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index c8b9cf87f4b..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index effdd0c4c84..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index f885ff46fa1..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index a6d55ca1380..00000000000
--- a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png
deleted file mode 100644
index 51cf047f966..00000000000
--- a/app/assets/images/emoji/man_with_turban.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png
deleted file mode 100644
index 1e12ee4b231..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png
deleted file mode 100644
index 37de4cceb23..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png
deleted file mode 100644
index f607afd3450..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png
deleted file mode 100644
index c05695888af..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png
deleted file mode 100644
index 4b4ff64720b..00000000000
--- a/app/assets/images/emoji/man_with_turban_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png
deleted file mode 100644
index 4bf7541032c..00000000000
--- a/app/assets/images/emoji/mans_shoe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png
deleted file mode 100644
index 15efe32c798..00000000000
--- a/app/assets/images/emoji/map.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png
deleted file mode 100644
index c49acea67f7..00000000000
--- a/app/assets/images/emoji/maple_leaf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png
deleted file mode 100644
index 8d6114761f6..00000000000
--- a/app/assets/images/emoji/martial_arts_uniform.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png
deleted file mode 100644
index 1e800acd1c0..00000000000
--- a/app/assets/images/emoji/mask.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png
deleted file mode 100644
index b91d845e374..00000000000
--- a/app/assets/images/emoji/massage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png
deleted file mode 100644
index e0f415d3186..00000000000
--- a/app/assets/images/emoji/massage_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png
deleted file mode 100644
index 0bb244a270b..00000000000
--- a/app/assets/images/emoji/massage_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png
deleted file mode 100644
index a117ee81a22..00000000000
--- a/app/assets/images/emoji/massage_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png
deleted file mode 100644
index 6f42ab017f4..00000000000
--- a/app/assets/images/emoji/massage_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png
deleted file mode 100644
index 6a388c0d0b5..00000000000
--- a/app/assets/images/emoji/massage_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png
deleted file mode 100644
index b20a59d1690..00000000000
--- a/app/assets/images/emoji/meat_on_bone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png
deleted file mode 100644
index b85896b14da..00000000000
--- a/app/assets/images/emoji/medal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png
deleted file mode 100644
index 4e6735188e3..00000000000
--- a/app/assets/images/emoji/mega.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png
deleted file mode 100644
index c01232d419d..00000000000
--- a/app/assets/images/emoji/melon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png
deleted file mode 100644
index b4297362869..00000000000
--- a/app/assets/images/emoji/menorah.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png
deleted file mode 100644
index f5a1e1ba0cd..00000000000
--- a/app/assets/images/emoji/mens.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png
deleted file mode 100644
index 4aa6e7e0a44..00000000000
--- a/app/assets/images/emoji/metal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png
deleted file mode 100644
index c080d2addbd..00000000000
--- a/app/assets/images/emoji/metal_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png
deleted file mode 100644
index 12313529bcf..00000000000
--- a/app/assets/images/emoji/metal_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png
deleted file mode 100644
index ca9be6ae67b..00000000000
--- a/app/assets/images/emoji/metal_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png
deleted file mode 100644
index abe28cbf890..00000000000
--- a/app/assets/images/emoji/metal_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png
deleted file mode 100644
index 0c6b5dd34ed..00000000000
--- a/app/assets/images/emoji/metal_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png
deleted file mode 100644
index 1de8f0551f3..00000000000
--- a/app/assets/images/emoji/metro.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png
deleted file mode 100644
index d4e6b0def25..00000000000
--- a/app/assets/images/emoji/microphone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png
deleted file mode 100644
index cd9167654ff..00000000000
--- a/app/assets/images/emoji/microphone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png
deleted file mode 100644
index 90f5acf6a78..00000000000
--- a/app/assets/images/emoji/microscope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png
deleted file mode 100644
index 697f7a25eb2..00000000000
--- a/app/assets/images/emoji/middle_finger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png
deleted file mode 100644
index 61ef12a1548..00000000000
--- a/app/assets/images/emoji/middle_finger_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png
deleted file mode 100644
index c31a69be9af..00000000000
--- a/app/assets/images/emoji/middle_finger_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png
deleted file mode 100644
index 73ac216ce63..00000000000
--- a/app/assets/images/emoji/middle_finger_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png
deleted file mode 100644
index 80b8ab7706d..00000000000
--- a/app/assets/images/emoji/middle_finger_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png
deleted file mode 100644
index a8826b196e8..00000000000
--- a/app/assets/images/emoji/middle_finger_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png
deleted file mode 100644
index ecd3fb03584..00000000000
--- a/app/assets/images/emoji/military_medal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png
deleted file mode 100644
index e4fcf2e64f3..00000000000
--- a/app/assets/images/emoji/milk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png
deleted file mode 100644
index b2b8ac59c5e..00000000000
--- a/app/assets/images/emoji/milky_way.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png
deleted file mode 100644
index c60dd8f47ab..00000000000
--- a/app/assets/images/emoji/minibus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png
deleted file mode 100644
index 9fa94cfbe74..00000000000
--- a/app/assets/images/emoji/minidisc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png
deleted file mode 100644
index 8b661ec1c94..00000000000
--- a/app/assets/images/emoji/mobile_phone_off.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png
deleted file mode 100644
index 75fd1e90cb0..00000000000
--- a/app/assets/images/emoji/money_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png
deleted file mode 100644
index f022b04b3c2..00000000000
--- a/app/assets/images/emoji/money_with_wings.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png
deleted file mode 100644
index b9296be0902..00000000000
--- a/app/assets/images/emoji/moneybag.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png
deleted file mode 100644
index 9fae29448e3..00000000000
--- a/app/assets/images/emoji/monkey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png
deleted file mode 100644
index 7cab9b91a82..00000000000
--- a/app/assets/images/emoji/monkey_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png
deleted file mode 100644
index 11eb1f574bf..00000000000
--- a/app/assets/images/emoji/monorail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png
deleted file mode 100644
index 8b17ddd9d00..00000000000
--- a/app/assets/images/emoji/mortar_board.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png
deleted file mode 100644
index ef770b26d96..00000000000
--- a/app/assets/images/emoji/mosque.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png
deleted file mode 100644
index c5afa72d807..00000000000
--- a/app/assets/images/emoji/motor_scooter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png
deleted file mode 100644
index 0506db1a40f..00000000000
--- a/app/assets/images/emoji/motorboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png
deleted file mode 100644
index 3d1d567e8ec..00000000000
--- a/app/assets/images/emoji/motorcycle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png
deleted file mode 100644
index 8c3d3d03e3f..00000000000
--- a/app/assets/images/emoji/motorway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png
deleted file mode 100644
index 88a54752458..00000000000
--- a/app/assets/images/emoji/mount_fuji.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png
deleted file mode 100644
index 6722ebdd294..00000000000
--- a/app/assets/images/emoji/mountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png
deleted file mode 100644
index 41d3dc3ac6f..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png
deleted file mode 100644
index e9f1daf5e40..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png
deleted file mode 100644
index 555b9e29d4d..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png
deleted file mode 100644
index 7df5508ec8c..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png
deleted file mode 100644
index f94b3450697..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png
deleted file mode 100644
index 16a45861e1f..00000000000
--- a/app/assets/images/emoji/mountain_bicyclist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png
deleted file mode 100644
index 1dea73ca53b..00000000000
--- a/app/assets/images/emoji/mountain_cableway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png
deleted file mode 100644
index ade2218e469..00000000000
--- a/app/assets/images/emoji/mountain_railway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png
deleted file mode 100644
index 76e1cfd8313..00000000000
--- a/app/assets/images/emoji/mountain_snow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png
deleted file mode 100644
index 50afcd3262e..00000000000
--- a/app/assets/images/emoji/mouse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png
deleted file mode 100644
index 20fb041f09f..00000000000
--- a/app/assets/images/emoji/mouse2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png
deleted file mode 100644
index e84e96ff6e8..00000000000
--- a/app/assets/images/emoji/mouse_three_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png
deleted file mode 100644
index 4e73b130155..00000000000
--- a/app/assets/images/emoji/movie_camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png
deleted file mode 100644
index e6a7779c45b..00000000000
--- a/app/assets/images/emoji/moyai.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
deleted file mode 100644
index 9cf2458df1a..00000000000
--- a/app/assets/images/emoji/mrs_claus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png
deleted file mode 100644
index d8a695d7035..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png
deleted file mode 100644
index 0e17e8c51f3..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png
deleted file mode 100644
index c3ee4d1dfae..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png
deleted file mode 100644
index 68a556da2fe..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png
deleted file mode 100644
index ccab3c40ff2..00000000000
--- a/app/assets/images/emoji/mrs_claus_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png
deleted file mode 100644
index 7e67c1880f7..00000000000
--- a/app/assets/images/emoji/muscle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png
deleted file mode 100644
index 1522942ce51..00000000000
--- a/app/assets/images/emoji/muscle_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png
deleted file mode 100644
index 569c6e832ca..00000000000
--- a/app/assets/images/emoji/muscle_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png
deleted file mode 100644
index 0a76b00fa89..00000000000
--- a/app/assets/images/emoji/muscle_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png
deleted file mode 100644
index f0cf31328e0..00000000000
--- a/app/assets/images/emoji/muscle_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png
deleted file mode 100644
index 4fda92460e8..00000000000
--- a/app/assets/images/emoji/muscle_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png
deleted file mode 100644
index dd85742ba2c..00000000000
--- a/app/assets/images/emoji/mushroom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png
deleted file mode 100644
index 442b7456842..00000000000
--- a/app/assets/images/emoji/musical_keyboard.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png
deleted file mode 100644
index 06691ef61bb..00000000000
--- a/app/assets/images/emoji/musical_note.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png
deleted file mode 100644
index 47dc05a8ef5..00000000000
--- a/app/assets/images/emoji/musical_score.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png
deleted file mode 100644
index 7c1788e5075..00000000000
--- a/app/assets/images/emoji/mute.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png
deleted file mode 100644
index aa52af7050d..00000000000
--- a/app/assets/images/emoji/nail_care.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png
deleted file mode 100644
index 26e883dd244..00000000000
--- a/app/assets/images/emoji/nail_care_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png
deleted file mode 100644
index 61257b47ea3..00000000000
--- a/app/assets/images/emoji/nail_care_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png
deleted file mode 100644
index 29871b05f62..00000000000
--- a/app/assets/images/emoji/nail_care_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png
deleted file mode 100644
index 2881de0b17d..00000000000
--- a/app/assets/images/emoji/nail_care_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png
deleted file mode 100644
index a0b7c0a45a6..00000000000
--- a/app/assets/images/emoji/nail_care_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png
deleted file mode 100644
index ec5ee213e20..00000000000
--- a/app/assets/images/emoji/name_badge.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png
deleted file mode 100644
index a566c109c28..00000000000
--- a/app/assets/images/emoji/nauseated_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png
deleted file mode 100644
index 1804e7f3ff3..00000000000
--- a/app/assets/images/emoji/necktie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png
deleted file mode 100644
index dae487f1f98..00000000000
--- a/app/assets/images/emoji/negative_squared_cross_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png
deleted file mode 100644
index 7820bd581dc..00000000000
--- a/app/assets/images/emoji/nerd.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png
deleted file mode 100644
index 065d193afe4..00000000000
--- a/app/assets/images/emoji/neutral_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png
deleted file mode 100644
index b4f85488d1a..00000000000
--- a/app/assets/images/emoji/new.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png
deleted file mode 100644
index ecff72caa42..00000000000
--- a/app/assets/images/emoji/new_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png
deleted file mode 100644
index 150dd12400c..00000000000
--- a/app/assets/images/emoji/new_moon_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png
deleted file mode 100644
index 2aa8f060bde..00000000000
--- a/app/assets/images/emoji/newspaper.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png
deleted file mode 100644
index f64748df2b2..00000000000
--- a/app/assets/images/emoji/newspaper2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png
deleted file mode 100644
index ee8d20f5ebc..00000000000
--- a/app/assets/images/emoji/ng.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png
deleted file mode 100644
index ca2018f456d..00000000000
--- a/app/assets/images/emoji/night_with_stars.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png
deleted file mode 100644
index 9fce3d1eca9..00000000000
--- a/app/assets/images/emoji/nine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png
deleted file mode 100644
index 15cb38dd1e7..00000000000
--- a/app/assets/images/emoji/no_bell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png
deleted file mode 100644
index 19c85421ce9..00000000000
--- a/app/assets/images/emoji/no_bicycles.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png
deleted file mode 100644
index 476800fc5c6..00000000000
--- a/app/assets/images/emoji/no_entry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png
deleted file mode 100644
index d2efd65e74b..00000000000
--- a/app/assets/images/emoji/no_entry_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png
deleted file mode 100644
index ed577100322..00000000000
--- a/app/assets/images/emoji/no_good.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png
deleted file mode 100644
index 5c1a3cbb884..00000000000
--- a/app/assets/images/emoji/no_good_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png
deleted file mode 100644
index 80d8021f8fe..00000000000
--- a/app/assets/images/emoji/no_good_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png
deleted file mode 100644
index 635e6a00815..00000000000
--- a/app/assets/images/emoji/no_good_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png
deleted file mode 100644
index b96e412a374..00000000000
--- a/app/assets/images/emoji/no_good_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png
deleted file mode 100644
index 9a7084afa0a..00000000000
--- a/app/assets/images/emoji/no_good_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png
deleted file mode 100644
index 7b1ae6ea579..00000000000
--- a/app/assets/images/emoji/no_mobile_phones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png
deleted file mode 100644
index b642f6c1172..00000000000
--- a/app/assets/images/emoji/no_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png
deleted file mode 100644
index 286aa577a23..00000000000
--- a/app/assets/images/emoji/no_pedestrians.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png
deleted file mode 100644
index 586b8d29d05..00000000000
--- a/app/assets/images/emoji/no_smoking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png
deleted file mode 100644
index 827d4193f4e..00000000000
--- a/app/assets/images/emoji/non-potable_water.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png
deleted file mode 100644
index 2f04ac5f98f..00000000000
--- a/app/assets/images/emoji/nose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png
deleted file mode 100644
index 8008d17506e..00000000000
--- a/app/assets/images/emoji/nose_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png
deleted file mode 100644
index ac17f26e827..00000000000
--- a/app/assets/images/emoji/nose_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png
deleted file mode 100644
index d8b6cbe0f8e..00000000000
--- a/app/assets/images/emoji/nose_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png
deleted file mode 100644
index 004b2631e2e..00000000000
--- a/app/assets/images/emoji/nose_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png
deleted file mode 100644
index 7b33821f6c9..00000000000
--- a/app/assets/images/emoji/nose_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png
deleted file mode 100644
index f6c28b4915d..00000000000
--- a/app/assets/images/emoji/notebook.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png
deleted file mode 100644
index 03f566b6d2c..00000000000
--- a/app/assets/images/emoji/notebook_with_decorative_cover.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png
deleted file mode 100644
index 85faa10d8ea..00000000000
--- a/app/assets/images/emoji/notepad_spiral.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png
deleted file mode 100644
index 57d499aa181..00000000000
--- a/app/assets/images/emoji/notes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png
deleted file mode 100644
index 4b9ae155319..00000000000
--- a/app/assets/images/emoji/nut_and_bolt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png
deleted file mode 100644
index 3fe75ce4675..00000000000
--- a/app/assets/images/emoji/o.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png
deleted file mode 100644
index 73278ba194a..00000000000
--- a/app/assets/images/emoji/o2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png
deleted file mode 100644
index 45ff1e87703..00000000000
--- a/app/assets/images/emoji/ocean.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png
deleted file mode 100644
index 5ed61004045..00000000000
--- a/app/assets/images/emoji/octagonal_sign.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png
deleted file mode 100644
index 72c84074aac..00000000000
--- a/app/assets/images/emoji/octopus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png
deleted file mode 100644
index d38a849fece..00000000000
--- a/app/assets/images/emoji/oden.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png
deleted file mode 100644
index 7eee927d1b0..00000000000
--- a/app/assets/images/emoji/office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png
deleted file mode 100644
index c4c4d42da8b..00000000000
--- a/app/assets/images/emoji/oil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png
deleted file mode 100644
index d0d775532ff..00000000000
--- a/app/assets/images/emoji/ok.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png
deleted file mode 100644
index 028d69b0de3..00000000000
--- a/app/assets/images/emoji/ok_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png
deleted file mode 100644
index cecf7b2ab5a..00000000000
--- a/app/assets/images/emoji/ok_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png
deleted file mode 100644
index c19239bcd3d..00000000000
--- a/app/assets/images/emoji/ok_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png
deleted file mode 100644
index 94b65b03ecd..00000000000
--- a/app/assets/images/emoji/ok_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png
deleted file mode 100644
index 03d26f08e6a..00000000000
--- a/app/assets/images/emoji/ok_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png
deleted file mode 100644
index d4b24086364..00000000000
--- a/app/assets/images/emoji/ok_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png
deleted file mode 100644
index 90a2c7469c4..00000000000
--- a/app/assets/images/emoji/ok_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png
deleted file mode 100644
index c99543e785b..00000000000
--- a/app/assets/images/emoji/ok_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png
deleted file mode 100644
index ad5fae813db..00000000000
--- a/app/assets/images/emoji/ok_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png
deleted file mode 100644
index 51bf4fab406..00000000000
--- a/app/assets/images/emoji/ok_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png
deleted file mode 100644
index ee3f9dc640a..00000000000
--- a/app/assets/images/emoji/ok_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png
deleted file mode 100644
index 62a9d9237f7..00000000000
--- a/app/assets/images/emoji/ok_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png
deleted file mode 100644
index 4ace4e6f308..00000000000
--- a/app/assets/images/emoji/older_man.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png
deleted file mode 100644
index ab459baace8..00000000000
--- a/app/assets/images/emoji/older_man_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png
deleted file mode 100644
index f4dfc7694ea..00000000000
--- a/app/assets/images/emoji/older_man_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png
deleted file mode 100644
index 5ffd11792f4..00000000000
--- a/app/assets/images/emoji/older_man_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png
deleted file mode 100644
index b350a764bfd..00000000000
--- a/app/assets/images/emoji/older_man_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png
deleted file mode 100644
index 05fe24a1708..00000000000
--- a/app/assets/images/emoji/older_man_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png
deleted file mode 100644
index 52dc4987143..00000000000
--- a/app/assets/images/emoji/older_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png
deleted file mode 100644
index b49e821402c..00000000000
--- a/app/assets/images/emoji/older_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png
deleted file mode 100644
index e86bf5ab3b7..00000000000
--- a/app/assets/images/emoji/older_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png
deleted file mode 100644
index 83fc14b0874..00000000000
--- a/app/assets/images/emoji/older_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png
deleted file mode 100644
index e4aa8a424d4..00000000000
--- a/app/assets/images/emoji/older_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png
deleted file mode 100644
index 4009012bb0a..00000000000
--- a/app/assets/images/emoji/older_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png
deleted file mode 100644
index a35c63c459c..00000000000
--- a/app/assets/images/emoji/om_symbol.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png
deleted file mode 100644
index a0c371ae21e..00000000000
--- a/app/assets/images/emoji/on.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png
deleted file mode 100644
index 3c7e1d52e63..00000000000
--- a/app/assets/images/emoji/oncoming_automobile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png
deleted file mode 100644
index ad91e256c7f..00000000000
--- a/app/assets/images/emoji/oncoming_bus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png
deleted file mode 100644
index c9109c85b5d..00000000000
--- a/app/assets/images/emoji/oncoming_police_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png
deleted file mode 100644
index fea14e45846..00000000000
--- a/app/assets/images/emoji/oncoming_taxi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png
deleted file mode 100644
index e6d84b80128..00000000000
--- a/app/assets/images/emoji/one.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png
deleted file mode 100644
index 3993b09222f..00000000000
--- a/app/assets/images/emoji/open_file_folder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png
deleted file mode 100644
index 1cf75c9101e..00000000000
--- a/app/assets/images/emoji/open_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png
deleted file mode 100644
index 352d2614f11..00000000000
--- a/app/assets/images/emoji/open_hands_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png
deleted file mode 100644
index 70824a50c73..00000000000
--- a/app/assets/images/emoji/open_hands_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png
deleted file mode 100644
index d7d136bd3db..00000000000
--- a/app/assets/images/emoji/open_hands_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png
deleted file mode 100644
index df4eaa711e7..00000000000
--- a/app/assets/images/emoji/open_hands_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png
deleted file mode 100644
index 7dc04eaebd8..00000000000
--- a/app/assets/images/emoji/open_hands_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png
deleted file mode 100644
index a62cd27e148..00000000000
--- a/app/assets/images/emoji/open_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png
deleted file mode 100644
index 0a780a700da..00000000000
--- a/app/assets/images/emoji/ophiuchus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png
deleted file mode 100644
index ab40e6ae6a2..00000000000
--- a/app/assets/images/emoji/orange_book.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png
deleted file mode 100644
index 0530e33a4d4..00000000000
--- a/app/assets/images/emoji/orthodox_cross.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png
deleted file mode 100644
index 46493ed5b2c..00000000000
--- a/app/assets/images/emoji/outbox_tray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png
deleted file mode 100644
index fa6815480c3..00000000000
--- a/app/assets/images/emoji/owl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png
deleted file mode 100644
index badf5708f2f..00000000000
--- a/app/assets/images/emoji/ox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png
deleted file mode 100644
index 85431756ad8..00000000000
--- a/app/assets/images/emoji/package.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png
deleted file mode 100644
index ba4ed757e01..00000000000
--- a/app/assets/images/emoji/page_facing_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png
deleted file mode 100644
index 06355319c74..00000000000
--- a/app/assets/images/emoji/page_with_curl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png
deleted file mode 100644
index b24b99306a2..00000000000
--- a/app/assets/images/emoji/pager.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png
deleted file mode 100644
index 28bffbaa3c9..00000000000
--- a/app/assets/images/emoji/paintbrush.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png
deleted file mode 100644
index 4bbb10f4f19..00000000000
--- a/app/assets/images/emoji/palm_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png
deleted file mode 100644
index 6223d1a28e9..00000000000
--- a/app/assets/images/emoji/pancakes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png
deleted file mode 100644
index 978382775ce..00000000000
--- a/app/assets/images/emoji/panda_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png
deleted file mode 100644
index 8cd8d4f8750..00000000000
--- a/app/assets/images/emoji/paperclip.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png
deleted file mode 100644
index 76021e8c705..00000000000
--- a/app/assets/images/emoji/paperclips.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png
deleted file mode 100644
index 63ec7016301..00000000000
--- a/app/assets/images/emoji/park.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png
deleted file mode 100644
index 7be7dac27e8..00000000000
--- a/app/assets/images/emoji/parking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png
deleted file mode 100644
index 70453d41528..00000000000
--- a/app/assets/images/emoji/part_alternation_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png
deleted file mode 100644
index a55e59c344c..00000000000
--- a/app/assets/images/emoji/partly_sunny.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png
deleted file mode 100644
index 079e34ee4d4..00000000000
--- a/app/assets/images/emoji/passport_control.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png
deleted file mode 100644
index 4f07e7ebfd7..00000000000
--- a/app/assets/images/emoji/pause_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png
deleted file mode 100644
index 86033faf477..00000000000
--- a/app/assets/images/emoji/peace.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png
deleted file mode 100644
index 9ab57cbb758..00000000000
--- a/app/assets/images/emoji/peach.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png
deleted file mode 100644
index b64fadad010..00000000000
--- a/app/assets/images/emoji/peanuts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png
deleted file mode 100644
index 3869f718bcf..00000000000
--- a/app/assets/images/emoji/pear.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png
deleted file mode 100644
index 6ef7a342433..00000000000
--- a/app/assets/images/emoji/pen_ballpoint.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png
deleted file mode 100644
index 3ca4bd2c231..00000000000
--- a/app/assets/images/emoji/pen_fountain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png
deleted file mode 100644
index edc6155e168..00000000000
--- a/app/assets/images/emoji/pencil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png
deleted file mode 100644
index 3833d590fa2..00000000000
--- a/app/assets/images/emoji/pencil2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png
deleted file mode 100644
index c0064fb9734..00000000000
--- a/app/assets/images/emoji/penguin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png
deleted file mode 100644
index 490fb566954..00000000000
--- a/app/assets/images/emoji/pensive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png
deleted file mode 100644
index 685441fdaa1..00000000000
--- a/app/assets/images/emoji/performing_arts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png
deleted file mode 100644
index 646a05fe908..00000000000
--- a/app/assets/images/emoji/persevere.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png
deleted file mode 100644
index 579324959a1..00000000000
--- a/app/assets/images/emoji/person_frowning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png
deleted file mode 100644
index 21d3bb43923..00000000000
--- a/app/assets/images/emoji/person_frowning_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png
deleted file mode 100644
index 973f5fc8382..00000000000
--- a/app/assets/images/emoji/person_frowning_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png
deleted file mode 100644
index 41fbcc78816..00000000000
--- a/app/assets/images/emoji/person_frowning_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png
deleted file mode 100644
index 5a37c741030..00000000000
--- a/app/assets/images/emoji/person_frowning_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png
deleted file mode 100644
index e08141f3efe..00000000000
--- a/app/assets/images/emoji/person_frowning_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png
deleted file mode 100644
index ad6f01a7dda..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png
deleted file mode 100644
index 7d18ef24445..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png
deleted file mode 100644
index dae1307315c..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png
deleted file mode 100644
index 684677e8e5a..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png
deleted file mode 100644
index 012be0b51f8..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png
deleted file mode 100644
index d4ecc4cf44b..00000000000
--- a/app/assets/images/emoji/person_with_blond_hair_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png
deleted file mode 100644
index 10eb0571078..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png
deleted file mode 100644
index 57e826b75a4..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png
deleted file mode 100644
index 3f317c0c25f..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png
deleted file mode 100644
index d2fbb6c20bf..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png
deleted file mode 100644
index 643ceb4a5c5..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png
deleted file mode 100644
index b2eb6859c32..00000000000
--- a/app/assets/images/emoji/person_with_pouting_face_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png
deleted file mode 100644
index 6370fe6d791..00000000000
--- a/app/assets/images/emoji/pick.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png
deleted file mode 100644
index afe05ca1676..00000000000
--- a/app/assets/images/emoji/pig.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png
deleted file mode 100644
index 5f31c1a2d75..00000000000
--- a/app/assets/images/emoji/pig2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png
deleted file mode 100644
index 3610ae4a910..00000000000
--- a/app/assets/images/emoji/pig_nose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png
deleted file mode 100644
index 1d4530e77a3..00000000000
--- a/app/assets/images/emoji/pill.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png
deleted file mode 100644
index c89a1606462..00000000000
--- a/app/assets/images/emoji/pineapple.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png
deleted file mode 100644
index ff3c51727d1..00000000000
--- a/app/assets/images/emoji/ping_pong.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png
deleted file mode 100644
index 7f6f646a95c..00000000000
--- a/app/assets/images/emoji/pisces.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png
deleted file mode 100644
index e07365cb398..00000000000
--- a/app/assets/images/emoji/pizza.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png
deleted file mode 100644
index 207d59cce85..00000000000
--- a/app/assets/images/emoji/place_of_worship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png
deleted file mode 100644
index a9f857139ac..00000000000
--- a/app/assets/images/emoji/play_pause.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png
deleted file mode 100644
index 00d3d13ab5c..00000000000
--- a/app/assets/images/emoji/point_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png
deleted file mode 100644
index 140f157d8c7..00000000000
--- a/app/assets/images/emoji/point_down_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png
deleted file mode 100644
index d518544f7fa..00000000000
--- a/app/assets/images/emoji/point_down_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png
deleted file mode 100644
index 018b688b8b7..00000000000
--- a/app/assets/images/emoji/point_down_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png
deleted file mode 100644
index 98845bf6f72..00000000000
--- a/app/assets/images/emoji/point_down_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png
deleted file mode 100644
index 9a9b039a9fc..00000000000
--- a/app/assets/images/emoji/point_down_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png
deleted file mode 100644
index 599fa2e3cf1..00000000000
--- a/app/assets/images/emoji/point_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png
deleted file mode 100644
index 88e2c306076..00000000000
--- a/app/assets/images/emoji/point_left_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png
deleted file mode 100644
index d3c89d87c5f..00000000000
--- a/app/assets/images/emoji/point_left_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png
deleted file mode 100644
index b23b9167358..00000000000
--- a/app/assets/images/emoji/point_left_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png
deleted file mode 100644
index 3093f325c27..00000000000
--- a/app/assets/images/emoji/point_left_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png
deleted file mode 100644
index 2b4cbfa120c..00000000000
--- a/app/assets/images/emoji/point_left_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png
deleted file mode 100644
index 93a3cd34aa5..00000000000
--- a/app/assets/images/emoji/point_right.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png
deleted file mode 100644
index 4a28c6bbc89..00000000000
--- a/app/assets/images/emoji/point_right_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png
deleted file mode 100644
index 7cb13231733..00000000000
--- a/app/assets/images/emoji/point_right_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png
deleted file mode 100644
index 5514807d71a..00000000000
--- a/app/assets/images/emoji/point_right_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png
deleted file mode 100644
index b8541d6440d..00000000000
--- a/app/assets/images/emoji/point_right_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png
deleted file mode 100644
index 1b7aab07bb1..00000000000
--- a/app/assets/images/emoji/point_right_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png
deleted file mode 100644
index f4978ff0f00..00000000000
--- a/app/assets/images/emoji/point_up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png
deleted file mode 100644
index bc496dfeae4..00000000000
--- a/app/assets/images/emoji/point_up_2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png
deleted file mode 100644
index a12a7e78430..00000000000
--- a/app/assets/images/emoji/point_up_2_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png
deleted file mode 100644
index cdff40ceab0..00000000000
--- a/app/assets/images/emoji/point_up_2_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png
deleted file mode 100644
index a07ce9e5ae8..00000000000
--- a/app/assets/images/emoji/point_up_2_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png
deleted file mode 100644
index 4f86c88ba42..00000000000
--- a/app/assets/images/emoji/point_up_2_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png
deleted file mode 100644
index ed1b26c35d3..00000000000
--- a/app/assets/images/emoji/point_up_2_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png
deleted file mode 100644
index 6a9db21d64c..00000000000
--- a/app/assets/images/emoji/point_up_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png
deleted file mode 100644
index 15aa9ea0e05..00000000000
--- a/app/assets/images/emoji/point_up_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png
deleted file mode 100644
index 652b73a9c5d..00000000000
--- a/app/assets/images/emoji/point_up_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png
deleted file mode 100644
index 692bad926e9..00000000000
--- a/app/assets/images/emoji/point_up_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png
deleted file mode 100644
index 1e1b10fb71c..00000000000
--- a/app/assets/images/emoji/point_up_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png
deleted file mode 100644
index 3da4253de7e..00000000000
--- a/app/assets/images/emoji/police_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png
deleted file mode 100644
index 8ec39e396af..00000000000
--- a/app/assets/images/emoji/poodle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png
deleted file mode 100644
index 10b15e72d56..00000000000
--- a/app/assets/images/emoji/poop.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png
deleted file mode 100644
index 36853e381d4..00000000000
--- a/app/assets/images/emoji/popcorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png
deleted file mode 100644
index a23848f9aa0..00000000000
--- a/app/assets/images/emoji/post_office.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png
deleted file mode 100644
index c173b8dbd67..00000000000
--- a/app/assets/images/emoji/postal_horn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png
deleted file mode 100644
index 07c9c4ab3d6..00000000000
--- a/app/assets/images/emoji/postbox.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png
deleted file mode 100644
index 2c610049459..00000000000
--- a/app/assets/images/emoji/potable_water.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png
deleted file mode 100644
index 70350ca2c0a..00000000000
--- a/app/assets/images/emoji/potato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png
deleted file mode 100644
index 8795c6c66ff..00000000000
--- a/app/assets/images/emoji/pouch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png
deleted file mode 100644
index eea4a53a2f9..00000000000
--- a/app/assets/images/emoji/poultry_leg.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png
deleted file mode 100644
index a0d4c4099e9..00000000000
--- a/app/assets/images/emoji/pound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png
deleted file mode 100644
index 41ddfeab42b..00000000000
--- a/app/assets/images/emoji/pouting_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png
deleted file mode 100644
index 8347f2435be..00000000000
--- a/app/assets/images/emoji/pray.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png
deleted file mode 100644
index 060ef257172..00000000000
--- a/app/assets/images/emoji/pray_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png
deleted file mode 100644
index 56dc607c07a..00000000000
--- a/app/assets/images/emoji/pray_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png
deleted file mode 100644
index 0f33b862008..00000000000
--- a/app/assets/images/emoji/pray_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png
deleted file mode 100644
index 2ea8dc11657..00000000000
--- a/app/assets/images/emoji/pray_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png
deleted file mode 100644
index 2128a6c4703..00000000000
--- a/app/assets/images/emoji/pray_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png
deleted file mode 100644
index a4b6dfcc62e..00000000000
--- a/app/assets/images/emoji/prayer_beads.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png
deleted file mode 100644
index 084e83a414a..00000000000
--- a/app/assets/images/emoji/pregnant_woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png
deleted file mode 100644
index a78703b33aa..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png
deleted file mode 100644
index 0068c6c4a77..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png
deleted file mode 100644
index 3206296b684..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png
deleted file mode 100644
index 120fda5cd8c..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png
deleted file mode 100644
index 569bfdf05ce..00000000000
--- a/app/assets/images/emoji/pregnant_woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png
deleted file mode 100644
index 38d69344c84..00000000000
--- a/app/assets/images/emoji/prince.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png
deleted file mode 100644
index 849930c8887..00000000000
--- a/app/assets/images/emoji/prince_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png
deleted file mode 100644
index 23d8b3b1285..00000000000
--- a/app/assets/images/emoji/prince_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png
deleted file mode 100644
index db6dfff0647..00000000000
--- a/app/assets/images/emoji/prince_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png
deleted file mode 100644
index 8e10f8be6a8..00000000000
--- a/app/assets/images/emoji/prince_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png
deleted file mode 100644
index 138d4ea7048..00000000000
--- a/app/assets/images/emoji/prince_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png
deleted file mode 100644
index 879e9fa8c5d..00000000000
--- a/app/assets/images/emoji/princess.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png
deleted file mode 100644
index c28078cdc36..00000000000
--- a/app/assets/images/emoji/princess_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png
deleted file mode 100644
index dcd20e6ecd4..00000000000
--- a/app/assets/images/emoji/princess_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png
deleted file mode 100644
index cde6f315c56..00000000000
--- a/app/assets/images/emoji/princess_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png
deleted file mode 100644
index c71e69caaef..00000000000
--- a/app/assets/images/emoji/princess_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png
deleted file mode 100644
index 063e2645910..00000000000
--- a/app/assets/images/emoji/princess_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png
deleted file mode 100644
index 027c830f0fe..00000000000
--- a/app/assets/images/emoji/printer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png
deleted file mode 100644
index ce9ab0daa28..00000000000
--- a/app/assets/images/emoji/projector.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png
deleted file mode 100644
index b14ca5f5211..00000000000
--- a/app/assets/images/emoji/punch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png
deleted file mode 100644
index 93c7d17fb47..00000000000
--- a/app/assets/images/emoji/punch_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png
deleted file mode 100644
index c0a1af6e10a..00000000000
--- a/app/assets/images/emoji/punch_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png
deleted file mode 100644
index 1458b021201..00000000000
--- a/app/assets/images/emoji/punch_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png
deleted file mode 100644
index c1466bfcdef..00000000000
--- a/app/assets/images/emoji/punch_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png
deleted file mode 100644
index 00b4ddb8953..00000000000
--- a/app/assets/images/emoji/punch_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png
deleted file mode 100644
index 95c53a9ade6..00000000000
--- a/app/assets/images/emoji/purple_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png
deleted file mode 100644
index 981346193c5..00000000000
--- a/app/assets/images/emoji/purse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png
deleted file mode 100644
index 57e07d7f4cc..00000000000
--- a/app/assets/images/emoji/pushpin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png
deleted file mode 100644
index 82a84f9a375..00000000000
--- a/app/assets/images/emoji/put_litter_in_its_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png
deleted file mode 100644
index 5a58f3458aa..00000000000
--- a/app/assets/images/emoji/question.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png
deleted file mode 100644
index ea75ab0426e..00000000000
--- a/app/assets/images/emoji/rabbit.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png
deleted file mode 100644
index 2c8a29c642f..00000000000
--- a/app/assets/images/emoji/rabbit2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png
deleted file mode 100644
index fe3f045f446..00000000000
--- a/app/assets/images/emoji/race_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png
deleted file mode 100644
index b3e73cc8903..00000000000
--- a/app/assets/images/emoji/racehorse.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png
deleted file mode 100644
index dec381fa242..00000000000
--- a/app/assets/images/emoji/radio.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png
deleted file mode 100644
index 3a23449d917..00000000000
--- a/app/assets/images/emoji/radio_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png
deleted file mode 100644
index 3b46199fe37..00000000000
--- a/app/assets/images/emoji/radioactive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png
deleted file mode 100644
index 9d739bd40ad..00000000000
--- a/app/assets/images/emoji/rage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png
deleted file mode 100644
index a9acbf13008..00000000000
--- a/app/assets/images/emoji/railway_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png
deleted file mode 100644
index e1a7a0d1430..00000000000
--- a/app/assets/images/emoji/railway_track.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png
deleted file mode 100644
index 154735d7147..00000000000
--- a/app/assets/images/emoji/rainbow.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png
deleted file mode 100644
index 479234294b4..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png
deleted file mode 100644
index 813d28499b5..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png
deleted file mode 100644
index 192ff795e37..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png
deleted file mode 100644
index 61a727abe6b..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png
deleted file mode 100644
index 2e83da511f5..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png
deleted file mode 100644
index d7a5b95a02c..00000000000
--- a/app/assets/images/emoji/raised_back_of_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png
deleted file mode 100644
index 6b2954315d1..00000000000
--- a/app/assets/images/emoji/raised_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png
deleted file mode 100644
index 3b752902c07..00000000000
--- a/app/assets/images/emoji/raised_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png
deleted file mode 100644
index 44e2a514c60..00000000000
--- a/app/assets/images/emoji/raised_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png
deleted file mode 100644
index 5bb62a7528a..00000000000
--- a/app/assets/images/emoji/raised_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png
deleted file mode 100644
index c7f8c9ec270..00000000000
--- a/app/assets/images/emoji/raised_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png
deleted file mode 100644
index c601b58a73e..00000000000
--- a/app/assets/images/emoji/raised_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png
deleted file mode 100644
index c0155f728e7..00000000000
--- a/app/assets/images/emoji/raised_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png
deleted file mode 100644
index 1168b8236b6..00000000000
--- a/app/assets/images/emoji/raised_hands_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png
deleted file mode 100644
index 322de622903..00000000000
--- a/app/assets/images/emoji/raised_hands_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png
deleted file mode 100644
index 2aa24e05ae1..00000000000
--- a/app/assets/images/emoji/raised_hands_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png
deleted file mode 100644
index f31bf0db992..00000000000
--- a/app/assets/images/emoji/raised_hands_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png
deleted file mode 100644
index 5e95067f98b..00000000000
--- a/app/assets/images/emoji/raised_hands_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png
deleted file mode 100644
index 2880708c0cc..00000000000
--- a/app/assets/images/emoji/raising_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png
deleted file mode 100644
index 1c90e3e2689..00000000000
--- a/app/assets/images/emoji/raising_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png
deleted file mode 100644
index 82c3ef2bfc5..00000000000
--- a/app/assets/images/emoji/raising_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png
deleted file mode 100644
index 1b1da2aa0ca..00000000000
--- a/app/assets/images/emoji/raising_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png
deleted file mode 100644
index e453855c01f..00000000000
--- a/app/assets/images/emoji/raising_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png
deleted file mode 100644
index b86200fd844..00000000000
--- a/app/assets/images/emoji/raising_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png
deleted file mode 100644
index 52a44464c9b..00000000000
--- a/app/assets/images/emoji/ram.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png
deleted file mode 100644
index c1cb7cd7384..00000000000
--- a/app/assets/images/emoji/ramen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png
deleted file mode 100644
index 86219144f10..00000000000
--- a/app/assets/images/emoji/rat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png
deleted file mode 100644
index ada52830fce..00000000000
--- a/app/assets/images/emoji/record_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png
deleted file mode 100644
index 9221f095c37..00000000000
--- a/app/assets/images/emoji/recycle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png
deleted file mode 100644
index b3e6a774dea..00000000000
--- a/app/assets/images/emoji/red_car.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png
deleted file mode 100644
index 4bef930d92f..00000000000
--- a/app/assets/images/emoji/red_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png
deleted file mode 100644
index 53ef9f2d4e6..00000000000
--- a/app/assets/images/emoji/registered.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png
deleted file mode 100644
index e9e53c03d45..00000000000
--- a/app/assets/images/emoji/relaxed.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png
deleted file mode 100644
index 715ad0bf53f..00000000000
--- a/app/assets/images/emoji/relieved.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png
deleted file mode 100644
index 3988bbd094c..00000000000
--- a/app/assets/images/emoji/reminder_ribbon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png
deleted file mode 100644
index 540ce4e0fba..00000000000
--- a/app/assets/images/emoji/repeat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png
deleted file mode 100644
index 9567e83337f..00000000000
--- a/app/assets/images/emoji/repeat_one.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png
deleted file mode 100644
index 9588e0f0ef7..00000000000
--- a/app/assets/images/emoji/restroom.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png
deleted file mode 100644
index 7b9d1948f73..00000000000
--- a/app/assets/images/emoji/revolving_hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png
deleted file mode 100644
index e22e2bd3da5..00000000000
--- a/app/assets/images/emoji/rewind.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png
deleted file mode 100644
index 12f4e0d9d9b..00000000000
--- a/app/assets/images/emoji/rhino.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png
deleted file mode 100644
index 0f253c3d8c8..00000000000
--- a/app/assets/images/emoji/ribbon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png
deleted file mode 100644
index 6e3ac7956b1..00000000000
--- a/app/assets/images/emoji/rice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png
deleted file mode 100644
index d3d8ee25cb8..00000000000
--- a/app/assets/images/emoji/rice_ball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png
deleted file mode 100644
index 7fbd08e4ff9..00000000000
--- a/app/assets/images/emoji/rice_cracker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png
deleted file mode 100644
index 1a28426592a..00000000000
--- a/app/assets/images/emoji/rice_scene.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png
deleted file mode 100644
index 754ed066d2c..00000000000
--- a/app/assets/images/emoji/right_facing_fist.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png
deleted file mode 100644
index 33ded2f61a6..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png
deleted file mode 100644
index 88054e335c7..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png
deleted file mode 100644
index 84b9f5da7f7..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png
deleted file mode 100644
index e741cfea68b..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png
deleted file mode 100644
index cf66d760c1f..00000000000
--- a/app/assets/images/emoji/right_facing_fist_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png
deleted file mode 100644
index 87d227adb74..00000000000
--- a/app/assets/images/emoji/ring.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png
deleted file mode 100644
index 7cc62612c6a..00000000000
--- a/app/assets/images/emoji/robot.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png
deleted file mode 100644
index 0d8da089a37..00000000000
--- a/app/assets/images/emoji/rocket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png
deleted file mode 100644
index b1736fedfeb..00000000000
--- a/app/assets/images/emoji/rofl.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png
deleted file mode 100644
index 5b849e071e8..00000000000
--- a/app/assets/images/emoji/roller_coaster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png
deleted file mode 100644
index 2f77b9fc3b9..00000000000
--- a/app/assets/images/emoji/rolling_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png
deleted file mode 100644
index bbf2bbff97a..00000000000
--- a/app/assets/images/emoji/rooster.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png
deleted file mode 100644
index 52c286d31ce..00000000000
--- a/app/assets/images/emoji/rose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png
deleted file mode 100644
index 8030e494bcf..00000000000
--- a/app/assets/images/emoji/rosette.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png
deleted file mode 100644
index cad66b0afef..00000000000
--- a/app/assets/images/emoji/rotating_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png
deleted file mode 100644
index 28b9d72866e..00000000000
--- a/app/assets/images/emoji/round_pushpin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png
deleted file mode 100644
index dd4dfc095d9..00000000000
--- a/app/assets/images/emoji/rowboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png
deleted file mode 100644
index 5e5d18548cb..00000000000
--- a/app/assets/images/emoji/rowboat_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png
deleted file mode 100644
index 9b123ef8871..00000000000
--- a/app/assets/images/emoji/rowboat_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png
deleted file mode 100644
index 8ebd89a55f5..00000000000
--- a/app/assets/images/emoji/rowboat_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png
deleted file mode 100644
index 2b0d04f8725..00000000000
--- a/app/assets/images/emoji/rowboat_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png
deleted file mode 100644
index b346f2dfc84..00000000000
--- a/app/assets/images/emoji/rowboat_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png
deleted file mode 100644
index b1872273436..00000000000
--- a/app/assets/images/emoji/rugby_football.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png
deleted file mode 100644
index e914915976a..00000000000
--- a/app/assets/images/emoji/runner.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png
deleted file mode 100644
index 9355239a52d..00000000000
--- a/app/assets/images/emoji/runner_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png
deleted file mode 100644
index 6112fd5c376..00000000000
--- a/app/assets/images/emoji/runner_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png
deleted file mode 100644
index 625ec708f48..00000000000
--- a/app/assets/images/emoji/runner_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png
deleted file mode 100644
index 242f1b56337..00000000000
--- a/app/assets/images/emoji/runner_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png
deleted file mode 100644
index 2976c6f019f..00000000000
--- a/app/assets/images/emoji/runner_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png
deleted file mode 100644
index 6d83c06b803..00000000000
--- a/app/assets/images/emoji/running_shirt_with_sash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png
deleted file mode 100644
index 900f9633247..00000000000
--- a/app/assets/images/emoji/sa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png
deleted file mode 100644
index f8d94ff2923..00000000000
--- a/app/assets/images/emoji/sagittarius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png
deleted file mode 100644
index 772ef11da5d..00000000000
--- a/app/assets/images/emoji/sailboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png
deleted file mode 100644
index 2933f5672c4..00000000000
--- a/app/assets/images/emoji/sake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png
deleted file mode 100644
index c89f9341158..00000000000
--- a/app/assets/images/emoji/salad.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png
deleted file mode 100644
index 9d9f5122b7a..00000000000
--- a/app/assets/images/emoji/sandal.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png
deleted file mode 100644
index bc83ab80d52..00000000000
--- a/app/assets/images/emoji/santa.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png
deleted file mode 100644
index 5233ffb7174..00000000000
--- a/app/assets/images/emoji/santa_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png
deleted file mode 100644
index 4e845438197..00000000000
--- a/app/assets/images/emoji/santa_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png
deleted file mode 100644
index 7fc4f33b60f..00000000000
--- a/app/assets/images/emoji/santa_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png
deleted file mode 100644
index d1d5a15132d..00000000000
--- a/app/assets/images/emoji/santa_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png
deleted file mode 100644
index 4d697a01f24..00000000000
--- a/app/assets/images/emoji/santa_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png
deleted file mode 100644
index db0372795f4..00000000000
--- a/app/assets/images/emoji/satellite.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png
deleted file mode 100644
index 4ba55d6e297..00000000000
--- a/app/assets/images/emoji/satellite_orbital.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png
deleted file mode 100644
index a392faec291..00000000000
--- a/app/assets/images/emoji/saxophone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png
deleted file mode 100644
index 0757eda1684..00000000000
--- a/app/assets/images/emoji/scales.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png
deleted file mode 100644
index 269759534f0..00000000000
--- a/app/assets/images/emoji/school.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png
deleted file mode 100644
index 9997c86e7dc..00000000000
--- a/app/assets/images/emoji/school_satchel.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png
deleted file mode 100644
index 270571c8cdd..00000000000
--- a/app/assets/images/emoji/scissors.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png
deleted file mode 100644
index 4ab7ef59cd2..00000000000
--- a/app/assets/images/emoji/scooter.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png
deleted file mode 100644
index 449a6b281c9..00000000000
--- a/app/assets/images/emoji/scorpion.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png
deleted file mode 100644
index c31a9920455..00000000000
--- a/app/assets/images/emoji/scorpius.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png
deleted file mode 100644
index c3bea9f2510..00000000000
--- a/app/assets/images/emoji/scream.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png
deleted file mode 100644
index 15803ad8e6e..00000000000
--- a/app/assets/images/emoji/scream_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png
deleted file mode 100644
index 50ee5dcd4b9..00000000000
--- a/app/assets/images/emoji/scroll.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png
deleted file mode 100644
index a6d72d95adb..00000000000
--- a/app/assets/images/emoji/seat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png
deleted file mode 100644
index 17b011268b6..00000000000
--- a/app/assets/images/emoji/second_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png
deleted file mode 100644
index 5fd72608e60..00000000000
--- a/app/assets/images/emoji/secret.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png
deleted file mode 100644
index 5187e474531..00000000000
--- a/app/assets/images/emoji/see_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png
deleted file mode 100644
index ae0948bcfd6..00000000000
--- a/app/assets/images/emoji/seedling.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png
deleted file mode 100644
index 6a1ba75c7e3..00000000000
--- a/app/assets/images/emoji/selfie.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png
deleted file mode 100644
index 290e075b56f..00000000000
--- a/app/assets/images/emoji/selfie_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png
deleted file mode 100644
index fcd9595b643..00000000000
--- a/app/assets/images/emoji/selfie_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png
deleted file mode 100644
index f3a22fdf435..00000000000
--- a/app/assets/images/emoji/selfie_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png
deleted file mode 100644
index cdecf6d9f4e..00000000000
--- a/app/assets/images/emoji/selfie_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png
deleted file mode 100644
index 86acbb6c202..00000000000
--- a/app/assets/images/emoji/selfie_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png
deleted file mode 100644
index 9b3476ae7c7..00000000000
--- a/app/assets/images/emoji/seven.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png
deleted file mode 100644
index 663a1006acd..00000000000
--- a/app/assets/images/emoji/shallow_pan_of_food.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png
deleted file mode 100644
index f202aecfe6f..00000000000
--- a/app/assets/images/emoji/shamrock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png
deleted file mode 100644
index c75076d57d8..00000000000
--- a/app/assets/images/emoji/shark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png
deleted file mode 100644
index 36dfb53ca93..00000000000
--- a/app/assets/images/emoji/shaved_ice.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png
deleted file mode 100644
index 102b8a52b28..00000000000
--- a/app/assets/images/emoji/sheep.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png
deleted file mode 100644
index 55721629f62..00000000000
--- a/app/assets/images/emoji/shell.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png
deleted file mode 100644
index 610bf033ce0..00000000000
--- a/app/assets/images/emoji/shield.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png
deleted file mode 100644
index 5a344975bf3..00000000000
--- a/app/assets/images/emoji/shinto_shrine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png
deleted file mode 100644
index 62d54f7d6c9..00000000000
--- a/app/assets/images/emoji/ship.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png
deleted file mode 100644
index af08dec8b59..00000000000
--- a/app/assets/images/emoji/shirt.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png
deleted file mode 100644
index 99f2a2b13ac..00000000000
--- a/app/assets/images/emoji/shopping_bags.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png
deleted file mode 100644
index 1086fe6e456..00000000000
--- a/app/assets/images/emoji/shopping_cart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png
deleted file mode 100644
index 156776a2e52..00000000000
--- a/app/assets/images/emoji/shower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png
deleted file mode 100644
index 49eff28a71e..00000000000
--- a/app/assets/images/emoji/shrimp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png
deleted file mode 100644
index 76e63bfac77..00000000000
--- a/app/assets/images/emoji/shrug.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png
deleted file mode 100644
index 1c895e64468..00000000000
--- a/app/assets/images/emoji/shrug_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png
deleted file mode 100644
index 4e3ca8f8bac..00000000000
--- a/app/assets/images/emoji/shrug_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png
deleted file mode 100644
index d1b16a19bb5..00000000000
--- a/app/assets/images/emoji/shrug_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png
deleted file mode 100644
index 5fbef3f2255..00000000000
--- a/app/assets/images/emoji/shrug_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png
deleted file mode 100644
index 4af2e28bc5c..00000000000
--- a/app/assets/images/emoji/shrug_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png
deleted file mode 100644
index ee2b5a4b519..00000000000
--- a/app/assets/images/emoji/signal_strength.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png
deleted file mode 100644
index 371b3acef2c..00000000000
--- a/app/assets/images/emoji/six.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png
deleted file mode 100644
index 2eb1707458b..00000000000
--- a/app/assets/images/emoji/six_pointed_star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png
deleted file mode 100644
index 4a2d2c12306..00000000000
--- a/app/assets/images/emoji/ski.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png
deleted file mode 100644
index 2eb3bdce2af..00000000000
--- a/app/assets/images/emoji/skier.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png
deleted file mode 100644
index 26abb17296a..00000000000
--- a/app/assets/images/emoji/skull.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png
deleted file mode 100644
index b459df9227a..00000000000
--- a/app/assets/images/emoji/skull_crossbones.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png
deleted file mode 100644
index 9ecf600d6d8..00000000000
--- a/app/assets/images/emoji/sleeping.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png
deleted file mode 100644
index c739e7fb69b..00000000000
--- a/app/assets/images/emoji/sleeping_accommodation.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png
deleted file mode 100644
index 836b4107717..00000000000
--- a/app/assets/images/emoji/sleepy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png
deleted file mode 100644
index b2f1d983d36..00000000000
--- a/app/assets/images/emoji/slight_frown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png
deleted file mode 100644
index ddd7d65dd3d..00000000000
--- a/app/assets/images/emoji/slight_smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png
deleted file mode 100644
index ee71b6c268c..00000000000
--- a/app/assets/images/emoji/slot_machine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png
deleted file mode 100644
index b86b5bc4db3..00000000000
--- a/app/assets/images/emoji/small_blue_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png
deleted file mode 100644
index e1c6ed9b2f8..00000000000
--- a/app/assets/images/emoji/small_orange_diamond.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png
deleted file mode 100644
index 785887c195a..00000000000
--- a/app/assets/images/emoji/small_red_triangle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png
deleted file mode 100644
index a83beff1914..00000000000
--- a/app/assets/images/emoji/small_red_triangle_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png
deleted file mode 100644
index aa47ffe978c..00000000000
--- a/app/assets/images/emoji/smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png
deleted file mode 100644
index 6f25f11dd3a..00000000000
--- a/app/assets/images/emoji/smile_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png
deleted file mode 100644
index 30957a65968..00000000000
--- a/app/assets/images/emoji/smiley.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png
deleted file mode 100644
index 163b57a3427..00000000000
--- a/app/assets/images/emoji/smiley_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png
deleted file mode 100644
index cc2c5f1ec72..00000000000
--- a/app/assets/images/emoji/smiling_imp.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png
deleted file mode 100644
index 87852109988..00000000000
--- a/app/assets/images/emoji/smirk.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png
deleted file mode 100644
index 9ac5954c199..00000000000
--- a/app/assets/images/emoji/smirk_cat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png
deleted file mode 100644
index 910f648c8f9..00000000000
--- a/app/assets/images/emoji/smoking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png
deleted file mode 100644
index f4ea071e2d3..00000000000
--- a/app/assets/images/emoji/snail.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png
deleted file mode 100644
index d0278a28d8c..00000000000
--- a/app/assets/images/emoji/snake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png
deleted file mode 100644
index ccf07d4b64d..00000000000
--- a/app/assets/images/emoji/sneezing_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png
deleted file mode 100644
index 6361c0f2c9d..00000000000
--- a/app/assets/images/emoji/snowboarder.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png
deleted file mode 100644
index db319a77ec6..00000000000
--- a/app/assets/images/emoji/snowflake.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png
deleted file mode 100644
index 20c177c2aff..00000000000
--- a/app/assets/images/emoji/snowman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png
deleted file mode 100644
index 896f28502af..00000000000
--- a/app/assets/images/emoji/snowman2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png
deleted file mode 100644
index 52e3517a1ee..00000000000
--- a/app/assets/images/emoji/sob.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png
deleted file mode 100644
index 28cfa218d6d..00000000000
--- a/app/assets/images/emoji/soccer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png
deleted file mode 100644
index 8cdfd86690d..00000000000
--- a/app/assets/images/emoji/soon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png
deleted file mode 100644
index d7d8c9953e4..00000000000
--- a/app/assets/images/emoji/sos.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png
deleted file mode 100644
index e75ddca53ba..00000000000
--- a/app/assets/images/emoji/sound.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png
deleted file mode 100644
index 2e73f5f32e5..00000000000
--- a/app/assets/images/emoji/space_invader.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png
deleted file mode 100644
index f822f184cb0..00000000000
--- a/app/assets/images/emoji/spades.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png
deleted file mode 100644
index 89c24a321f1..00000000000
--- a/app/assets/images/emoji/spaghetti.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png
deleted file mode 100644
index 6aa7b6ec9cf..00000000000
--- a/app/assets/images/emoji/sparkle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png
deleted file mode 100644
index 30339cd6e09..00000000000
--- a/app/assets/images/emoji/sparkler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png
deleted file mode 100644
index 169bc10b023..00000000000
--- a/app/assets/images/emoji/sparkles.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png
deleted file mode 100644
index 6709269454e..00000000000
--- a/app/assets/images/emoji/sparkling_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png
deleted file mode 100644
index 9d9e07c974b..00000000000
--- a/app/assets/images/emoji/speak_no_evil.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png
deleted file mode 100644
index 7bcffb8fc43..00000000000
--- a/app/assets/images/emoji/speaker.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png
deleted file mode 100644
index 2df93aaae09..00000000000
--- a/app/assets/images/emoji/speaking_head.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png
deleted file mode 100644
index a34ef741733..00000000000
--- a/app/assets/images/emoji/speech_balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speech_left.png b/app/assets/images/emoji/speech_left.png
deleted file mode 100644
index 00c05959bcd..00000000000
--- a/app/assets/images/emoji/speech_left.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png
deleted file mode 100644
index 74059d12de1..00000000000
--- a/app/assets/images/emoji/speedboat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png
deleted file mode 100644
index 3849fa90b94..00000000000
--- a/app/assets/images/emoji/spider.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png
deleted file mode 100644
index ba448ee7fba..00000000000
--- a/app/assets/images/emoji/spider_web.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png
deleted file mode 100644
index 3c4da766aee..00000000000
--- a/app/assets/images/emoji/spoon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png
deleted file mode 100644
index a729e9584d6..00000000000
--- a/app/assets/images/emoji/spy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png
deleted file mode 100644
index 2d1c022caee..00000000000
--- a/app/assets/images/emoji/spy_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png
deleted file mode 100644
index 548b9c26f5d..00000000000
--- a/app/assets/images/emoji/spy_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png
deleted file mode 100644
index b023f4b18e1..00000000000
--- a/app/assets/images/emoji/spy_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png
deleted file mode 100644
index d8300af492d..00000000000
--- a/app/assets/images/emoji/spy_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png
deleted file mode 100644
index ca1462595fa..00000000000
--- a/app/assets/images/emoji/spy_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png
deleted file mode 100644
index d2af223f0cb..00000000000
--- a/app/assets/images/emoji/squid.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png
deleted file mode 100644
index 00cd6db5e29..00000000000
--- a/app/assets/images/emoji/stadium.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png
deleted file mode 100644
index c930947076e..00000000000
--- a/app/assets/images/emoji/star.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png
deleted file mode 100644
index 2f5cba592db..00000000000
--- a/app/assets/images/emoji/star2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png
deleted file mode 100644
index e182636457d..00000000000
--- a/app/assets/images/emoji/star_and_crescent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png
deleted file mode 100644
index fc59d0dde24..00000000000
--- a/app/assets/images/emoji/star_of_david.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png
deleted file mode 100644
index aa45384d1c6..00000000000
--- a/app/assets/images/emoji/stars.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png
deleted file mode 100644
index 5c26fee529c..00000000000
--- a/app/assets/images/emoji/station.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png
deleted file mode 100644
index 05df8289b59..00000000000
--- a/app/assets/images/emoji/statue_of_liberty.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png
deleted file mode 100644
index 9ac0d999c4c..00000000000
--- a/app/assets/images/emoji/steam_locomotive.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png
deleted file mode 100644
index 6b3f010c17a..00000000000
--- a/app/assets/images/emoji/stew.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png
deleted file mode 100644
index cfa99988ac2..00000000000
--- a/app/assets/images/emoji/stop_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png
deleted file mode 100644
index 8fae1c9a898..00000000000
--- a/app/assets/images/emoji/stopwatch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png
deleted file mode 100644
index 1017b7433a1..00000000000
--- a/app/assets/images/emoji/straight_ruler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png
deleted file mode 100644
index 7bb86f0b29c..00000000000
--- a/app/assets/images/emoji/strawberry.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png
deleted file mode 100644
index 25757341f96..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
deleted file mode 100644
index 5c0401e9b1d..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
deleted file mode 100644
index 4817eaa3dc6..00000000000
--- a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png
deleted file mode 100644
index a2e10df40a5..00000000000
--- a/app/assets/images/emoji/stuffed_flatbread.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png
deleted file mode 100644
index 14a4ea971db..00000000000
--- a/app/assets/images/emoji/sun_with_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png
deleted file mode 100644
index 08cc07761ea..00000000000
--- a/app/assets/images/emoji/sunflower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png
deleted file mode 100644
index 20011735110..00000000000
--- a/app/assets/images/emoji/sunglasses.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png
deleted file mode 100644
index fd521ae31a7..00000000000
--- a/app/assets/images/emoji/sunny.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png
deleted file mode 100644
index 4ad36003c20..00000000000
--- a/app/assets/images/emoji/sunrise.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png
deleted file mode 100644
index 2b99307344d..00000000000
--- a/app/assets/images/emoji/sunrise_over_mountains.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png
deleted file mode 100644
index 3ab017adf4b..00000000000
--- a/app/assets/images/emoji/surfer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png
deleted file mode 100644
index b5faaa524cc..00000000000
--- a/app/assets/images/emoji/surfer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png
deleted file mode 100644
index 6d92e412ff1..00000000000
--- a/app/assets/images/emoji/surfer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png
deleted file mode 100644
index f05ef59496e..00000000000
--- a/app/assets/images/emoji/surfer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png
deleted file mode 100644
index 35e143d19dc..00000000000
--- a/app/assets/images/emoji/surfer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png
deleted file mode 100644
index 38917658eac..00000000000
--- a/app/assets/images/emoji/surfer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png
deleted file mode 100644
index f171fd2f7a1..00000000000
--- a/app/assets/images/emoji/sushi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png
deleted file mode 100644
index a59d5f48c24..00000000000
--- a/app/assets/images/emoji/suspension_railway.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png
deleted file mode 100644
index f0dae7b7893..00000000000
--- a/app/assets/images/emoji/sweat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png
deleted file mode 100644
index 4106117ebc8..00000000000
--- a/app/assets/images/emoji/sweat_drops.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png
deleted file mode 100644
index cb18d9c899b..00000000000
--- a/app/assets/images/emoji/sweat_smile.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png
deleted file mode 100644
index 92a425f2e20..00000000000
--- a/app/assets/images/emoji/sweet_potato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png
deleted file mode 100644
index 55b4d72f9a7..00000000000
--- a/app/assets/images/emoji/swimmer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png
deleted file mode 100644
index 38441c9ca9a..00000000000
--- a/app/assets/images/emoji/swimmer_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png
deleted file mode 100644
index b0d43112444..00000000000
--- a/app/assets/images/emoji/swimmer_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png
deleted file mode 100644
index 211e77e2aa0..00000000000
--- a/app/assets/images/emoji/swimmer_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png
deleted file mode 100644
index f34c34db9d2..00000000000
--- a/app/assets/images/emoji/swimmer_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png
deleted file mode 100644
index 3e9231ff868..00000000000
--- a/app/assets/images/emoji/swimmer_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png
deleted file mode 100644
index ac2fc1f358f..00000000000
--- a/app/assets/images/emoji/symbols.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png
deleted file mode 100644
index ee347904c80..00000000000
--- a/app/assets/images/emoji/synagogue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png
deleted file mode 100644
index 71c1a9528d5..00000000000
--- a/app/assets/images/emoji/syringe.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png
deleted file mode 100644
index 10e847a4619..00000000000
--- a/app/assets/images/emoji/taco.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png
deleted file mode 100644
index 0244d60f269..00000000000
--- a/app/assets/images/emoji/tada.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png
deleted file mode 100644
index 46fcb3a1aac..00000000000
--- a/app/assets/images/emoji/tanabata_tree.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png
deleted file mode 100644
index ab14e5378db..00000000000
--- a/app/assets/images/emoji/tangerine.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png
deleted file mode 100644
index b2a370df42b..00000000000
--- a/app/assets/images/emoji/taurus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png
deleted file mode 100644
index 55f4cc84797..00000000000
--- a/app/assets/images/emoji/taxi.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png
deleted file mode 100644
index b53b98f0c45..00000000000
--- a/app/assets/images/emoji/tea.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png
deleted file mode 100644
index a1e69f566bc..00000000000
--- a/app/assets/images/emoji/telephone.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png
deleted file mode 100644
index 69388316c35..00000000000
--- a/app/assets/images/emoji/telephone_receiver.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png
deleted file mode 100644
index d63154614b5..00000000000
--- a/app/assets/images/emoji/telescope.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png
deleted file mode 100644
index 782d4004962..00000000000
--- a/app/assets/images/emoji/ten.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png
deleted file mode 100644
index 7e68ba8f301..00000000000
--- a/app/assets/images/emoji/tennis.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png
deleted file mode 100644
index 3fddcfc56eb..00000000000
--- a/app/assets/images/emoji/tent.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png
deleted file mode 100644
index b1147392426..00000000000
--- a/app/assets/images/emoji/thermometer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png
deleted file mode 100644
index 8fc57387563..00000000000
--- a/app/assets/images/emoji/thermometer_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png
deleted file mode 100644
index c18f6fd14ad..00000000000
--- a/app/assets/images/emoji/thinking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png
deleted file mode 100644
index 636e04a5950..00000000000
--- a/app/assets/images/emoji/third_place.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png
deleted file mode 100644
index 72fe8fa7022..00000000000
--- a/app/assets/images/emoji/thought_balloon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png
deleted file mode 100644
index dbaa6183e72..00000000000
--- a/app/assets/images/emoji/three.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png
deleted file mode 100644
index b63da2f20a8..00000000000
--- a/app/assets/images/emoji/thumbsdown.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png
deleted file mode 100644
index a1631af8e92..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png
deleted file mode 100644
index 85fff82d595..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png
deleted file mode 100644
index eeba3be80fd..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png
deleted file mode 100644
index 1addafdaed0..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png
deleted file mode 100644
index 37ec07b5721..00000000000
--- a/app/assets/images/emoji/thumbsdown_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png
deleted file mode 100644
index f9e6f13a34f..00000000000
--- a/app/assets/images/emoji/thumbsup.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png
deleted file mode 100644
index 39684cd5cc7..00000000000
--- a/app/assets/images/emoji/thumbsup_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png
deleted file mode 100644
index a9b59723573..00000000000
--- a/app/assets/images/emoji/thumbsup_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png
deleted file mode 100644
index c5e29167015..00000000000
--- a/app/assets/images/emoji/thumbsup_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png
deleted file mode 100644
index 5bf4857a884..00000000000
--- a/app/assets/images/emoji/thumbsup_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png
deleted file mode 100644
index d829f787c61..00000000000
--- a/app/assets/images/emoji/thumbsup_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png
deleted file mode 100644
index 31a26a1b6ee..00000000000
--- a/app/assets/images/emoji/thunder_cloud_rain.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png
deleted file mode 100644
index 605936bb6b3..00000000000
--- a/app/assets/images/emoji/ticket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png
deleted file mode 100644
index e510f4a7a50..00000000000
--- a/app/assets/images/emoji/tickets.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png
deleted file mode 100644
index a4d3ef086d4..00000000000
--- a/app/assets/images/emoji/tiger.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png
deleted file mode 100644
index 871a8b74d56..00000000000
--- a/app/assets/images/emoji/tiger2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png
deleted file mode 100644
index 8a3be574c24..00000000000
--- a/app/assets/images/emoji/timer.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png
deleted file mode 100644
index 4e01eff5b23..00000000000
--- a/app/assets/images/emoji/tired_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png
deleted file mode 100644
index 7a0c44a2c2b..00000000000
--- a/app/assets/images/emoji/tm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png
deleted file mode 100644
index 1392f761835..00000000000
--- a/app/assets/images/emoji/toilet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png
deleted file mode 100644
index 37df7fc65b1..00000000000
--- a/app/assets/images/emoji/tokyo_tower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png
deleted file mode 100644
index 497da8f6b22..00000000000
--- a/app/assets/images/emoji/tomato.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png
deleted file mode 100644
index c395f3d0d68..00000000000
--- a/app/assets/images/emoji/tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png
deleted file mode 100644
index 080847431c1..00000000000
--- a/app/assets/images/emoji/tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png
deleted file mode 100644
index 482dd403475..00000000000
--- a/app/assets/images/emoji/tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png
deleted file mode 100644
index 5cae8bb20b0..00000000000
--- a/app/assets/images/emoji/tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png
deleted file mode 100644
index 49d1a8c3a64..00000000000
--- a/app/assets/images/emoji/tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png
deleted file mode 100644
index 70ce9c1225f..00000000000
--- a/app/assets/images/emoji/tongue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png
deleted file mode 100644
index 3c6049273a9..00000000000
--- a/app/assets/images/emoji/tools.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png
deleted file mode 100644
index 49dea8c08b5..00000000000
--- a/app/assets/images/emoji/top.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png
deleted file mode 100644
index 131b657b109..00000000000
--- a/app/assets/images/emoji/tophat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png
deleted file mode 100644
index f8880d33bab..00000000000
--- a/app/assets/images/emoji/track_next.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png
deleted file mode 100644
index 1ffd0566cfc..00000000000
--- a/app/assets/images/emoji/track_previous.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png
deleted file mode 100644
index 3bea84ad7ce..00000000000
--- a/app/assets/images/emoji/trackball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png
deleted file mode 100644
index c1bf8cae44f..00000000000
--- a/app/assets/images/emoji/tractor.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png
deleted file mode 100644
index 6b312285b00..00000000000
--- a/app/assets/images/emoji/traffic_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png
deleted file mode 100644
index 3c80321f7e8..00000000000
--- a/app/assets/images/emoji/train.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png
deleted file mode 100644
index 367c7bc5d39..00000000000
--- a/app/assets/images/emoji/train2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png
deleted file mode 100644
index b6f0e69038f..00000000000
--- a/app/assets/images/emoji/tram.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png
deleted file mode 100644
index c12d8b06886..00000000000
--- a/app/assets/images/emoji/triangular_flag_on_post.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png
deleted file mode 100644
index 77dee9ee843..00000000000
--- a/app/assets/images/emoji/triangular_ruler.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png
deleted file mode 100644
index 777a1dad121..00000000000
--- a/app/assets/images/emoji/trident.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png
deleted file mode 100644
index 0be7a501969..00000000000
--- a/app/assets/images/emoji/triumph.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png
deleted file mode 100644
index 139a9931b52..00000000000
--- a/app/assets/images/emoji/trolleybus.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png
deleted file mode 100644
index ac2895c1896..00000000000
--- a/app/assets/images/emoji/trophy.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png
deleted file mode 100644
index cd714f81b36..00000000000
--- a/app/assets/images/emoji/tropical_drink.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png
deleted file mode 100644
index 252105235a6..00000000000
--- a/app/assets/images/emoji/tropical_fish.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png
deleted file mode 100644
index 130de047f8b..00000000000
--- a/app/assets/images/emoji/truck.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png
deleted file mode 100644
index 864ccbcd04a..00000000000
--- a/app/assets/images/emoji/trumpet.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png
deleted file mode 100644
index f799d75c182..00000000000
--- a/app/assets/images/emoji/tulip.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png
deleted file mode 100644
index 7bf09229879..00000000000
--- a/app/assets/images/emoji/tumbler_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png
deleted file mode 100644
index 344af94c9ec..00000000000
--- a/app/assets/images/emoji/turkey.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png
deleted file mode 100644
index c22f7519fe8..00000000000
--- a/app/assets/images/emoji/turtle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png
deleted file mode 100644
index 999f1fb5c6d..00000000000
--- a/app/assets/images/emoji/tv.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png
deleted file mode 100644
index 5904badde65..00000000000
--- a/app/assets/images/emoji/twisted_rightwards_arrows.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png
deleted file mode 100644
index 927339c9bff..00000000000
--- a/app/assets/images/emoji/two.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png
deleted file mode 100644
index 4d8c3386042..00000000000
--- a/app/assets/images/emoji/two_hearts.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png
deleted file mode 100644
index a511fda822a..00000000000
--- a/app/assets/images/emoji/two_men_holding_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png
deleted file mode 100644
index b077cd3e40f..00000000000
--- a/app/assets/images/emoji/two_women_holding_hands.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png
deleted file mode 100644
index c4f837fe684..00000000000
--- a/app/assets/images/emoji/u5272.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png
deleted file mode 100644
index 8375ad9d9af..00000000000
--- a/app/assets/images/emoji/u5408.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png
deleted file mode 100644
index d21cb30eaf3..00000000000
--- a/app/assets/images/emoji/u55b6.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png
deleted file mode 100644
index 078e23e4ff3..00000000000
--- a/app/assets/images/emoji/u6307.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png
deleted file mode 100644
index c41bd36a26a..00000000000
--- a/app/assets/images/emoji/u6708.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png
deleted file mode 100644
index a4510de41c0..00000000000
--- a/app/assets/images/emoji/u6709.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png
deleted file mode 100644
index f9dea8b8833..00000000000
--- a/app/assets/images/emoji/u6e80.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png
deleted file mode 100644
index d3a19b420de..00000000000
--- a/app/assets/images/emoji/u7121.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png
deleted file mode 100644
index 6b7af0ee222..00000000000
--- a/app/assets/images/emoji/u7533.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png
deleted file mode 100644
index 4c704e03433..00000000000
--- a/app/assets/images/emoji/u7981.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png
deleted file mode 100644
index 47966c1ea93..00000000000
--- a/app/assets/images/emoji/u7a7a.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png
deleted file mode 100644
index 5b35b7ff6a4..00000000000
--- a/app/assets/images/emoji/umbrella.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png
deleted file mode 100644
index 97fe859e74f..00000000000
--- a/app/assets/images/emoji/umbrella2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png
deleted file mode 100644
index 25e3677f2eb..00000000000
--- a/app/assets/images/emoji/unamused.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png
deleted file mode 100644
index 6dfe6da51e2..00000000000
--- a/app/assets/images/emoji/underage.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png
deleted file mode 100644
index 05a97969f7e..00000000000
--- a/app/assets/images/emoji/unicorn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png
deleted file mode 100644
index 4a74a693911..00000000000
--- a/app/assets/images/emoji/unlock.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png
deleted file mode 100644
index 0d42142ba04..00000000000
--- a/app/assets/images/emoji/up.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png
deleted file mode 100644
index 128f31c9828..00000000000
--- a/app/assets/images/emoji/upside_down.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png
deleted file mode 100644
index 6b5b3503438..00000000000
--- a/app/assets/images/emoji/urn.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png
deleted file mode 100644
index 70c5516ffee..00000000000
--- a/app/assets/images/emoji/v.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png
deleted file mode 100644
index 6ac54a745f4..00000000000
--- a/app/assets/images/emoji/v_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png
deleted file mode 100644
index 6dd9669866d..00000000000
--- a/app/assets/images/emoji/v_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png
deleted file mode 100644
index a615e53f02f..00000000000
--- a/app/assets/images/emoji/v_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png
deleted file mode 100644
index 33a34bd5a78..00000000000
--- a/app/assets/images/emoji/v_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png
deleted file mode 100644
index 45ad14b6c9c..00000000000
--- a/app/assets/images/emoji/v_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png
deleted file mode 100644
index 8085973eecf..00000000000
--- a/app/assets/images/emoji/vertical_traffic_light.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png
deleted file mode 100644
index b9eb78ecd92..00000000000
--- a/app/assets/images/emoji/vhs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png
deleted file mode 100644
index cc46510e48e..00000000000
--- a/app/assets/images/emoji/vibration_mode.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png
deleted file mode 100644
index 85b300d425c..00000000000
--- a/app/assets/images/emoji/video_camera.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png
deleted file mode 100644
index 316a9106a55..00000000000
--- a/app/assets/images/emoji/video_game.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png
deleted file mode 100644
index e1e76cce242..00000000000
--- a/app/assets/images/emoji/violin.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png
deleted file mode 100644
index a6b56c2cb5e..00000000000
--- a/app/assets/images/emoji/virgo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png
deleted file mode 100644
index 931d569294c..00000000000
--- a/app/assets/images/emoji/volcano.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png
deleted file mode 100644
index 7a0e49d4b07..00000000000
--- a/app/assets/images/emoji/volleyball.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png
deleted file mode 100644
index e1180f4a464..00000000000
--- a/app/assets/images/emoji/vs.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png
deleted file mode 100644
index 54728bcaf5c..00000000000
--- a/app/assets/images/emoji/vulcan.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png
deleted file mode 100644
index 8aff5d8fa16..00000000000
--- a/app/assets/images/emoji/vulcan_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png
deleted file mode 100644
index 82b7ad519b4..00000000000
--- a/app/assets/images/emoji/vulcan_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png
deleted file mode 100644
index d1400e1dd28..00000000000
--- a/app/assets/images/emoji/vulcan_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png
deleted file mode 100644
index 47e2b280148..00000000000
--- a/app/assets/images/emoji/vulcan_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png
deleted file mode 100644
index 60b5c6077be..00000000000
--- a/app/assets/images/emoji/vulcan_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png
deleted file mode 100644
index 06dc169a3fd..00000000000
--- a/app/assets/images/emoji/walking.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png
deleted file mode 100644
index 4e391b45a0b..00000000000
--- a/app/assets/images/emoji/walking_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png
deleted file mode 100644
index 31f94a1bce1..00000000000
--- a/app/assets/images/emoji/walking_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png
deleted file mode 100644
index f7ed8e39c2e..00000000000
--- a/app/assets/images/emoji/walking_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png
deleted file mode 100644
index e58dc04c7b2..00000000000
--- a/app/assets/images/emoji/walking_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png
deleted file mode 100644
index ba4e1b58fcb..00000000000
--- a/app/assets/images/emoji/walking_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png
deleted file mode 100644
index cf68706b871..00000000000
--- a/app/assets/images/emoji/waning_crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png
deleted file mode 100644
index 24e16266119..00000000000
--- a/app/assets/images/emoji/waning_gibbous_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png
deleted file mode 100644
index 35691c2ed97..00000000000
--- a/app/assets/images/emoji/warning.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png
deleted file mode 100644
index 2b3c484b498..00000000000
--- a/app/assets/images/emoji/wastebasket.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png
deleted file mode 100644
index 64819bc6e21..00000000000
--- a/app/assets/images/emoji/watch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png
deleted file mode 100644
index 80446615caf..00000000000
--- a/app/assets/images/emoji/water_buffalo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png
deleted file mode 100644
index cb44576780d..00000000000
--- a/app/assets/images/emoji/water_polo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png
deleted file mode 100644
index bed1a908d6a..00000000000
--- a/app/assets/images/emoji/water_polo_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png
deleted file mode 100644
index ec5a43b4d4a..00000000000
--- a/app/assets/images/emoji/water_polo_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png
deleted file mode 100644
index b081a4a5a96..00000000000
--- a/app/assets/images/emoji/water_polo_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png
deleted file mode 100644
index 82cfbc3b0c7..00000000000
--- a/app/assets/images/emoji/water_polo_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png
deleted file mode 100644
index bd3366eb06c..00000000000
--- a/app/assets/images/emoji/water_polo_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png
deleted file mode 100644
index 0761488b4c9..00000000000
--- a/app/assets/images/emoji/watermelon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png
deleted file mode 100644
index e0cd79b45f5..00000000000
--- a/app/assets/images/emoji/wave.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png
deleted file mode 100644
index 6b2b34b106e..00000000000
--- a/app/assets/images/emoji/wave_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png
deleted file mode 100644
index b857119732e..00000000000
--- a/app/assets/images/emoji/wave_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png
deleted file mode 100644
index 6283b670f43..00000000000
--- a/app/assets/images/emoji/wave_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png
deleted file mode 100644
index fe6b2baa747..00000000000
--- a/app/assets/images/emoji/wave_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png
deleted file mode 100644
index 4bd168ebb78..00000000000
--- a/app/assets/images/emoji/wave_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png
deleted file mode 100644
index 001c8d6e47d..00000000000
--- a/app/assets/images/emoji/wavy_dash.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png
deleted file mode 100644
index 687125173d9..00000000000
--- a/app/assets/images/emoji/waxing_crescent_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png
deleted file mode 100644
index 3a808156318..00000000000
--- a/app/assets/images/emoji/waxing_gibbous_moon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png
deleted file mode 100644
index aa433e84ba6..00000000000
--- a/app/assets/images/emoji/wc.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png
deleted file mode 100644
index 98bfbd24a16..00000000000
--- a/app/assets/images/emoji/weary.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png
deleted file mode 100644
index d0d8aa0bfae..00000000000
--- a/app/assets/images/emoji/wedding.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png
deleted file mode 100644
index 9f19b44257c..00000000000
--- a/app/assets/images/emoji/whale.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png
deleted file mode 100644
index 0df9d3c73a4..00000000000
--- a/app/assets/images/emoji/whale2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png
deleted file mode 100644
index 3666db0016b..00000000000
--- a/app/assets/images/emoji/wheel_of_dharma.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png
deleted file mode 100644
index 4e5b2698eac..00000000000
--- a/app/assets/images/emoji/wheelchair.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png
deleted file mode 100644
index e55f087e544..00000000000
--- a/app/assets/images/emoji/white_check_mark.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png
deleted file mode 100644
index c19e15684dd..00000000000
--- a/app/assets/images/emoji/white_circle.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png
deleted file mode 100644
index d6af8b60077..00000000000
--- a/app/assets/images/emoji/white_flower.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png
deleted file mode 100644
index 6f06c1c79de..00000000000
--- a/app/assets/images/emoji/white_large_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png
deleted file mode 100644
index ae874126750..00000000000
--- a/app/assets/images/emoji/white_medium_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png
deleted file mode 100644
index 8daacf57059..00000000000
--- a/app/assets/images/emoji/white_medium_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png
deleted file mode 100644
index d7ebdb0c0ed..00000000000
--- a/app/assets/images/emoji/white_small_square.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png
deleted file mode 100644
index 934b1cedfd2..00000000000
--- a/app/assets/images/emoji/white_square_button.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png
deleted file mode 100644
index 0a4cc100269..00000000000
--- a/app/assets/images/emoji/white_sun_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png
deleted file mode 100644
index 491f9ca4839..00000000000
--- a/app/assets/images/emoji/white_sun_rain_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png
deleted file mode 100644
index cead0bfa521..00000000000
--- a/app/assets/images/emoji/white_sun_small_cloud.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png
deleted file mode 100644
index 62412b143ae..00000000000
--- a/app/assets/images/emoji/wilted_rose.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png
deleted file mode 100644
index df81b652eb6..00000000000
--- a/app/assets/images/emoji/wind_blowing_face.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png
deleted file mode 100644
index 3c9ef3a95f6..00000000000
--- a/app/assets/images/emoji/wind_chime.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png
deleted file mode 100644
index 3cc98689192..00000000000
--- a/app/assets/images/emoji/wine_glass.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png
deleted file mode 100644
index 7ea7810a37d..00000000000
--- a/app/assets/images/emoji/wink.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png
deleted file mode 100644
index ba7220f2de9..00000000000
--- a/app/assets/images/emoji/wolf.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png
deleted file mode 100644
index ece440e7a61..00000000000
--- a/app/assets/images/emoji/woman.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png
deleted file mode 100644
index ff089b8889b..00000000000
--- a/app/assets/images/emoji/woman_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png
deleted file mode 100644
index 0719c378016..00000000000
--- a/app/assets/images/emoji/woman_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png
deleted file mode 100644
index 5672e2fd52d..00000000000
--- a/app/assets/images/emoji/woman_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png
deleted file mode 100644
index 5754aab558b..00000000000
--- a/app/assets/images/emoji/woman_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png
deleted file mode 100644
index fc252af3a39..00000000000
--- a/app/assets/images/emoji/woman_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png
deleted file mode 100644
index 01410dc8107..00000000000
--- a/app/assets/images/emoji/womans_clothes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png
deleted file mode 100644
index b837b6a2e47..00000000000
--- a/app/assets/images/emoji/womans_hat.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png
deleted file mode 100644
index d4ecc22e7b3..00000000000
--- a/app/assets/images/emoji/womens.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png
deleted file mode 100644
index 7074afcf5b7..00000000000
--- a/app/assets/images/emoji/worried.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png
deleted file mode 100644
index c16b7439697..00000000000
--- a/app/assets/images/emoji/wrench.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png
deleted file mode 100644
index 71e67cfad85..00000000000
--- a/app/assets/images/emoji/wrestlers.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png
deleted file mode 100644
index 379070fd03b..00000000000
--- a/app/assets/images/emoji/wrestlers_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png
deleted file mode 100644
index 6863ea9209d..00000000000
--- a/app/assets/images/emoji/wrestlers_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png
deleted file mode 100644
index b7e62910127..00000000000
--- a/app/assets/images/emoji/wrestlers_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png
deleted file mode 100644
index 750f9589233..00000000000
--- a/app/assets/images/emoji/wrestlers_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png
deleted file mode 100644
index 36ab9bb3f42..00000000000
--- a/app/assets/images/emoji/wrestlers_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png
deleted file mode 100644
index 85639f8ac40..00000000000
--- a/app/assets/images/emoji/writing_hand.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png
deleted file mode 100644
index 7923d8ebb17..00000000000
--- a/app/assets/images/emoji/writing_hand_tone1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png
deleted file mode 100644
index bcb304e15d2..00000000000
--- a/app/assets/images/emoji/writing_hand_tone2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png
deleted file mode 100644
index fd885fd2d90..00000000000
--- a/app/assets/images/emoji/writing_hand_tone3.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png
deleted file mode 100644
index d065b8c64ab..00000000000
--- a/app/assets/images/emoji/writing_hand_tone4.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png
deleted file mode 100644
index a44b3dd757c..00000000000
--- a/app/assets/images/emoji/writing_hand_tone5.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png
deleted file mode 100644
index 9f9ed0f7ad2..00000000000
--- a/app/assets/images/emoji/x.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png
deleted file mode 100644
index 7901a9d0103..00000000000
--- a/app/assets/images/emoji/yellow_heart.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png
deleted file mode 100644
index 63ee4799d66..00000000000
--- a/app/assets/images/emoji/yen.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png
deleted file mode 100644
index f2900f6338f..00000000000
--- a/app/assets/images/emoji/yin_yang.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png
deleted file mode 100644
index 2df15753ca1..00000000000
--- a/app/assets/images/emoji/yum.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png
deleted file mode 100644
index 47e68e48e49..00000000000
--- a/app/assets/images/emoji/zap.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png
deleted file mode 100644
index 13aca83e018..00000000000
--- a/app/assets/images/emoji/zero.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png
deleted file mode 100644
index f8ced2502a7..00000000000
--- a/app/assets/images/emoji/zipper_mouth.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png
deleted file mode 100644
index 9bc72b4469f..00000000000
--- a/app/assets/images/emoji/zzz.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d1396b6c4bc..8754c253881 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,12 +5,14 @@ import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
+ groupMembersPath: '/api/:version/groups/:id/members',
subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
+ projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
@@ -40,6 +42,12 @@ const Api = {
});
},
+ groupMembers(id) {
+ const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
@@ -104,6 +112,22 @@ const Api = {
return axios.get(url);
},
+ /**
+ * Get all Merge Requests for a project, eventually filtering based on
+ * supplied parameters
+ * @param projectPath
+ * @param params
+ * @returns {Promise}
+ */
+ projectMergeRequests(projectPath, params = {}) {
+ const url = Api.buildUrl(Api.projectMergeRequestsPath).replace(
+ ':id',
+ encodeURIComponent(projectPath),
+ );
+
+ return axios.get(url, { params });
+ },
+
// Return Merge Request for project
projectMergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.projectMergeRequestPath)
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index cace8bb9dba..aeb88715c11 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -437,7 +437,7 @@ export class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
- <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
+ <button class="btn award-control js-emoji-btn has-tooltip active" title="You">
${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span>
</button>
@@ -615,10 +615,18 @@ export class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
- awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
- const awardsHandler = new AwardsHandler(Emoji);
- awardsHandler.bindEvents();
- return awardsHandler;
+ awardsHandlerPromise = new Promise((resolve, reject) => {
+ import(/* webpackChunkName: 'emoji' */ './emoji')
+ .then(Emoji => {
+ Emoji.initEmojiMap()
+ .then(() => {
+ const awardsHandler = new AwardsHandler(Emoji);
+ awardsHandler.bindEvents();
+ resolve(awardsHandler);
+ })
+ .catch(() => reject);
+ })
+ .catch(() => reject);
});
}
return awardsHandlerPromise;
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 85a15b38de1..df74eb2c2f7 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -90,7 +90,7 @@ export default {
},
badgeImageUrlExample() {
const exampleUrl =
- 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg';
+ 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/pipeline.svg';
return sprintf(s__('Badges|e.g. %{exampleUrl}'), {
exampleUrl,
});
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 9051be1e102..cad5611c8c5 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -55,7 +55,7 @@ export default {
:disabled="badge.isDeleting"
class="btn btn-default append-right-8"
type="button"
- @click="editBadge(badge);"
+ @click="editBadge(badge)"
>
<icon :size="16" :aria-label="__('Edit')" name="pencil" />
</button>
@@ -65,7 +65,7 @@ export default {
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
- @click="updateBadgeInModal(badge);"
+ @click="updateBadgeInModal(badge)"
>
<icon :size="16" :aria-label="__('Delete')" name="remove" />
</button>
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 56293d5f96f..9034563d9b3 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -1,51 +1,79 @@
-import installCustomElements from 'document-register-element';
+import 'document-register-element';
import isEmojiUnicodeSupported from '../emoji/support';
+import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
-installCustomElements(window);
+class GlEmoji extends HTMLElement {
+ constructor() {
+ super();
-export default 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 (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
- // CSS sprite fallback takes precedence over image fallback
- if (hasCssSpriteFalback) {
- if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
- const emojiSpriteLinkTag = document.createElement('link');
- emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
- emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
- document.head.appendChild(emojiSpriteLinkTag);
- gon.emoji_sprites_css_added = true;
- }
- // IE 11 doesn't like adding multiple at once :(
- this.classList.add('emoji-icon');
- this.classList.add(fallbackSpriteClass);
- } else {
- import(/* webpackChunkName: 'emoji' */ '../emoji')
- .then(({ emojiImageTag, emojiFallbackImageSrc }) => {
- if (hasImageFallback) {
- this.innerHTML = emojiImageTag(name, fallbackSrc);
+ let emojiUnicode = this.textContent.trim();
+ const { fallbackSpriteClass, fallbackSrc, forceFallback } = this.dataset;
+ let { name, unicodeVersion } = this.dataset;
+
+ initEmojiMap()
+ .then(() => {
+ if (!unicodeVersion) {
+ const emojiInfo = getEmojiInfo(name);
+
+ if (emojiInfo) {
+ if (name !== emojiInfo.name) {
+ ({ name } = emojiInfo);
+ this.dataset.name = emojiInfo.name;
+ }
+ unicodeVersion = emojiInfo.u;
+ this.dataset.uni = unicodeVersion;
+
+ if (forceFallback === 'true' && !fallbackSpriteClass) {
+ this.innerHTML = emojiImageTag(name, emojiFallbackImageSrc(name));
} else {
- const src = emojiFallbackImageSrc(name);
- this.innerHTML = emojiImageTag(name, src);
+ emojiUnicode = emojiInfo.e;
+ this.innerHTML = emojiInfo.e;
}
- })
- .catch(() => {
- // do nothing
- });
- }
- }
- };
-
- document.registerElement('gl-emoji', {
- prototype: GlEmojiElementProto,
- });
+
+ this.title = emojiInfo.d;
+ }
+ }
+
+ 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 (
+ emojiUnicode &&
+ isEmojiUnicode &&
+ !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
+ ) {
+ // CSS sprite fallback takes precedence over image fallback
+ if (hasCssSpriteFalback) {
+ if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
+ const emojiSpriteLinkTag = document.createElement('link');
+ emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
+ emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
+ document.head.appendChild(emojiSpriteLinkTag);
+ gon.emoji_sprites_css_added = true;
+ }
+ // 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 = emojiFallbackImageSrc(name);
+ this.innerHTML = emojiImageTag(name, src);
+ }
+ }
+ })
+ .catch(error => {
+ // Only reject is already handled in initEmojiMap
+ throw error;
+ });
+ }
+}
+
+export default function installGlEmojiElement() {
+ if (!customElements.get('gl-emoji')) {
+ customElements.define('gl-emoji', GlEmoji);
+ }
}
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index fe02096d903..58cf057b2c2 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,320 +1,5 @@
-/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */
-
import $ from 'jquery';
-import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
-import { placeholderImage } from '~/lazy_loader';
-
-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) {
- return `[${el.checked ? 'x' : ' '}]`;
- },
- },
- ReferenceFilter: {
- '.tooltip'(el) {
- 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) {
- return '[[_TOC_]]';
- },
- },
- EmojiFilter: {
- 'img.emoji'(el) {
- return el.getAttribute('alt');
- },
- 'gl-emoji'(el) {
- return `:${el.getAttribute('data-name')}:`;
- },
- },
- ImageLinkFilter: {
- 'a.no-attachment-icon'(el, text) {
- return text;
- },
- },
- ImageLazyLoadFilter: {
- img(el, text) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
- },
- },
- VideoLinkFilter: {
- '.video-container'(el) {
- const videoEl = el.querySelector('video');
- if (!videoEl) return false;
-
- return CopyAsGFM.nodeToGFM(videoEl);
- },
- video(el) {
- return `![${el.dataset.title}](${el.getAttribute('src')})`;
- },
- },
- MermaidFilter: {
- 'svg.mermaid'(el, text) {
- const sourceEl = el.querySelector('text.source');
- if (!sourceEl) return false;
-
- return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
- },
- 'svg.mermaid style, svg.mermaid g'(el, text) {
- // We don't want to include the content of these elements in the copied text.
- return '';
- },
- },
- 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) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
- },
- 'span.katex-mathml'(el) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
- },
- 'span.katex-html'(el) {
- // 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) {
- return el.outerHTML;
- },
- dl(el, text) {
- let lines = text
- .replace(/\n\n/g, '\n')
- .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>\n`;
- },
- 'dt, dd, summary, details'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>\n`;
- },
- 'sup, sub, 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.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) {
- // 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) {
- const imageSrc = el.src;
- const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
- return `![${el.getAttribute('alt')}](${imageUrl})`;
- },
- '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(/^- /gm, '1. ');
- },
- h1(el, text) {
- return `# ${text.trim()}\n`;
- },
- h2(el, text) {
- return `## ${text.trim()}\n`;
- },
- h3(el, text) {
- return `### ${text.trim()}\n`;
- },
- h4(el, text) {
- return `#### ${text.trim()}\n`;
- },
- h5(el, text) {
- return `##### ${text.trim()}\n`;
- },
- h6(el, text) {
- return `###### ${text.trim()}\n`;
- },
- strong(el, text) {
- return `**${text}**`;
- },
- em(el, text) {
- return `_${text}_`;
- },
- del(el, text) {
- return `~~${text}~~`;
- },
- hr(el) {
- // extra leading \n is to ensure that there is a blank line between
- // a list followed by an hr, otherwise this breaks old redcarpet rendering
- return '\n-----\n';
- },
- p(el, text) {
- return `${text.trim()}\n`;
- },
- table(el) {
- 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].join('\n');
- },
- thead(el, text) {
- const cells = _.map(el.querySelectorAll('th'), cell => {
- let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
-
- let before = '';
- let after = '';
- const alignment = cell.align || cell.style.textAlign;
-
- switch (alignment) {
- 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;
- });
-
- const separatorRow = `|${cells.join('|')}|`;
-
- return [text, separatorRow].join('\n');
- },
- tr(el) {
- const cellEls = el.querySelectorAll('td, th');
- if (cellEls.length === 0) return false;
-
- const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
- return `| ${cells.join(' | ')} |`;
- },
- },
-};
+import { getSelectedFragment } from '~/lib/utils/common_utils';
export class CopyAsGFM {
constructor() {
@@ -347,8 +32,24 @@ export class CopyAsGFM {
e.preventDefault();
e.stopPropagation();
+ const div = document.createElement('div');
+ div.appendChild(el.cloneNode(true));
+ const html = div.innerHTML;
+
clipboardData.setData('text/plain', el.textContent);
- clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
+ clipboardData.setData('text/html', html);
+ // We are also setting this as fallback to transform the selection to gfm on paste
+ clipboardData.setData('text/x-gfm-html', html);
+
+ CopyAsGFM.nodeToGFM(el)
+ .then(res => {
+ clipboardData.setData('text/x-gfm', res);
+ })
+ .catch(() => {
+ // Not showing the error as Firefox might doesn't allow
+ // it or other browsers who have a time limit on the execution
+ // of the copy event
+ });
}
static pasteGFM(e) {
@@ -357,11 +58,28 @@ export class CopyAsGFM {
const text = clipboardData.getData('text/plain');
const gfm = clipboardData.getData('text/x-gfm');
- if (!gfm) return;
+ const gfmHtml = clipboardData.getData('text/x-gfm-html');
+ if (!gfm && !gfmHtml) return;
e.preventDefault();
- window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
+ // We have the original selection already converted to gfm
+ if (gfm) {
+ CopyAsGFM.insertPastedText(e.target, text, gfm);
+ } else {
+ // Due to the async copy call we are not able to produce gfm so we transform the cached HTML
+ const div = document.createElement('div');
+ div.innerHTML = gfmHtml;
+ CopyAsGFM.nodeToGFM(div)
+ .then(transformedGfm => {
+ CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
+ })
+ .catch(() => {});
+ }
+ }
+
+ static insertPastedText(target, text, gfm) {
+ window.gl.utils.insertText(target, textBefore => {
// If the text before the cursor contains an odd number of backticks,
// we are either inside an inline code span that starts with 1 backtick
// or a code block that starts with 3 backticks.
@@ -443,75 +161,24 @@ export class CopyAsGFM {
return codeElement;
}
- static nodeToGFM(node, respectWhitespaceParam = false) {
- if (node.nodeType === Node.COMMENT_NODE) {
- return '';
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent;
- }
-
- const respectWhitespace =
- respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
-
- const text = this.innerGFM(node, respectWhitespace);
-
- 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 (!nodeMatchesSelector(node, selector)) continue;
-
- let result;
- if (func.length === 2) {
- // if `func` takes 2 arguments, it depends on text.
- // if there is no text, we don't need to generate GFM for this node.
- if (text.length === 0) continue;
-
- result = func(node, text);
- } else {
- result = func(node);
- }
-
- if (result === false) continue;
-
- return result;
- }
- }
-
- return text;
- }
-
- static innerGFM(parentNode, respectWhitespace = false) {
- 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, respectWhitespace);
-
- // `clonedNode.replaceWith(text)` is not yet widely supported
- clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
- }
-
- let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
-
- if (!respectWhitespace) {
- nodeText = nodeText.trim();
- }
-
- return nodeText;
+ static nodeToGFM(node) {
+ return Promise.all([
+ import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
+ import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
+ ])
+ .then(([prosemirrorModel, schema, markdownSerializer]) => {
+ const { DOMParser } = prosemirrorModel;
+ const wrapEl = document.createElement('div');
+ wrapEl.appendChild(node.cloneNode(true));
+ const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
+
+ const res = markdownSerializer.default.serialize(doc, {
+ tightLists: true,
+ });
+ return res;
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
new file mode 100644
index 00000000000..47e5fc65c48
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
@@ -0,0 +1,106 @@
+import Doc from './nodes/doc';
+import Paragraph from './nodes/paragraph';
+import Text from './nodes/text';
+
+import Blockquote from './nodes/blockquote';
+import CodeBlock from './nodes/code_block';
+import HardBreak from './nodes/hard_break';
+import Heading from './nodes/heading';
+import HorizontalRule from './nodes/horizontal_rule';
+import Image from './nodes/image';
+
+import Table from './nodes/table';
+import TableHead from './nodes/table_head';
+import TableBody from './nodes/table_body';
+import TableHeaderRow from './nodes/table_header_row';
+import TableRow from './nodes/table_row';
+import TableCell from './nodes/table_cell';
+
+import Emoji from './nodes/emoji';
+import Reference from './nodes/reference';
+
+import TableOfContents from './nodes/table_of_contents';
+import Video from './nodes/video';
+
+import BulletList from './nodes/bullet_list';
+import OrderedList from './nodes/ordered_list';
+import ListItem from './nodes/list_item';
+
+import DescriptionList from './nodes/description_list';
+import DescriptionTerm from './nodes/description_term';
+import DescriptionDetails from './nodes/description_details';
+
+import TaskList from './nodes/task_list';
+import OrderedTaskList from './nodes/ordered_task_list';
+import TaskListItem from './nodes/task_list_item';
+
+import Summary from './nodes/summary';
+import Details from './nodes/details';
+
+import Bold from './marks/bold';
+import Italic from './marks/italic';
+import Strike from './marks/strike';
+import InlineDiff from './marks/inline_diff';
+
+import Link from './marks/link';
+import Code from './marks/code';
+import MathMark from './marks/math';
+import InlineHTML from './marks/inline_html';
+
+// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
+// GitLab Flavored Markdown (GFM) to HTML.
+// The nodes and marks referenced here transform 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 node or mark here.
+// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+
+export default [
+ new Doc(),
+ new Paragraph(),
+ new Text(),
+
+ new Blockquote(),
+ new CodeBlock(),
+ new HardBreak(),
+ new Heading({ maxLevel: 6 }),
+ new HorizontalRule(),
+ new Image(),
+
+ new Table(),
+ new TableHead(),
+ new TableBody(),
+ new TableHeaderRow(),
+ new TableRow(),
+ new TableCell(),
+
+ new Emoji(),
+ new Reference(),
+
+ new TableOfContents(),
+ new Video(),
+
+ new BulletList(),
+ new OrderedList(),
+ new ListItem(),
+
+ new DescriptionList(),
+ new DescriptionTerm(),
+ new DescriptionDetails(),
+
+ new TaskList(),
+ new OrderedTaskList(),
+ new TaskListItem(),
+
+ new Summary(),
+ new Details(),
+
+ new Bold(),
+ new Italic(),
+ new Strike(),
+ new InlineDiff(),
+
+ new Link(),
+ new Code(),
+ new MathMark(),
+ new InlineHTML(),
+];
diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js
new file mode 100644
index 00000000000..b537954c1cb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/bold.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Bold as BaseBold } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Bold extends BaseBold {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.strong;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js
new file mode 100644
index 00000000000..a760ee80dd0
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/code.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Code as BaseCode } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Code extends BaseCode {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.code;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
new file mode 100644
index 00000000000..ce425e80cd3
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter
+export default class InlineDiff extends Mark {
+ get name() {
+ return 'inline_diff';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ addition: {
+ default: true,
+ },
+ },
+ parseDOM: [
+ { tag: 'span.idiff.addition', attrs: { addition: true } },
+ { tag: 'span.idiff.deletion', attrs: { addition: false } },
+ ],
+ toDOM: node => [
+ 'span',
+ { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` },
+ 0,
+ ],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark) {
+ return mark.attrs.addition ? '{+' : '{-';
+ },
+ close(state, mark) {
+ return mark.attrs.addition ? '+}' : '-}';
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
new file mode 100644
index 00000000000..ebed8698e21
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
@@ -0,0 +1,46 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+import _ from 'underscore';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class InlineHTML extends Mark {
+ get name() {
+ return 'inline_html';
+ }
+
+ get schema() {
+ return {
+ excludes: '',
+ attrs: {
+ tag: {},
+ title: { default: null },
+ },
+ parseDOM: [
+ {
+ tag: 'sup, sub, kbd, q, samp, var',
+ getAttrs: el => ({ tag: el.nodeName.toLowerCase() }),
+ },
+ {
+ tag: 'abbr',
+ getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }),
+ },
+ ],
+ toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark) {
+ return `<${mark.attrs.tag}${
+ mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : ''
+ }>`;
+ },
+ close(state, mark) {
+ return `</${mark.attrs.tag}>`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js
new file mode 100644
index 00000000000..44b35c97739
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/italic.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Italic as BaseItalic } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Italic extends BaseItalic {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.em;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js
new file mode 100644
index 00000000000..5c23d6a5ceb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/link.js
@@ -0,0 +1,21 @@
+/* eslint-disable class-methods-use-this */
+
+import { Link as BaseLink } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Link extends BaseLink {
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark, parent, index) {
+ const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index);
+ return open === '<' ? '' : open;
+ },
+ close(state, mark, parent, index) {
+ const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index);
+ return close === '>' ? '' : close;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js
new file mode 100644
index 00000000000..e582fb18f15
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/math.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
+export default class MathMark extends Mark {
+ get name() {
+ return 'math';
+ }
+
+ get schema() {
+ return {
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::MathFilter
+ {
+ tag: 'code.code.math[data-math-style=inline]',
+ priority: 51,
+ },
+ // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex',
+ contentElement: 'annotation[encoding="application/x-tex"]',
+ },
+ ],
+ toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ escape: false,
+ open(state, mark, parent, index) {
+ return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`;
+ },
+ close(state, mark, parent, index) {
+ return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js
new file mode 100644
index 00000000000..c2951a40a4b
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js
@@ -0,0 +1,15 @@
+/* eslint-disable class-methods-use-this */
+
+import { Strike as BaseStrike } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Strike extends BaseStrike {
+ get toMarkdown() {
+ return {
+ open: '~~',
+ close: '~~',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
new file mode 100644
index 00000000000..b0bc8f79643
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
@@ -0,0 +1,13 @@
+/* eslint-disable class-methods-use-this */
+
+import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Blockquote extends BaseBlockquote {
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ defaultMarkdownSerializer.nodes.blockquote(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
new file mode 100644
index 00000000000..3b0792e1af8
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { BulletList as BaseBulletList } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class BulletList extends BaseBulletList {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.bullet_list(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
new file mode 100644
index 00000000000..1e0c05eff08
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
@@ -0,0 +1,99 @@
+/* eslint-disable class-methods-use-this */
+
+import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions';
+
+const PLAINTEXT_LANG = 'plaintext';
+
+// Transforms generated HTML back to GFM for:
+// - Banzai::Filter::SyntaxHighlightFilter
+// - Banzai::Filter::MathFilter
+// - Banzai::Filter::MermaidFilter
+// - Banzai::Filter::SuggestionFilter
+export default class CodeBlock extends BaseCodeBlock {
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ group: 'block',
+ code: true,
+ defining: true,
+ attrs: {
+ lang: { default: PLAINTEXT_LANG },
+ },
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter
+ {
+ tag: 'pre.code.highlight',
+ preserveWhitespace: 'full',
+ getAttrs: el => {
+ const lang = el.getAttribute('lang');
+ if (!lang || lang === '') return {};
+
+ return { lang };
+ },
+ },
+ // Matches HTML generated by Banzai::Filter::MathFilter,
+ // after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex-display',
+ preserveWhitespace: 'full',
+ contentElement: 'annotation[encoding="application/x-tex"]',
+ attrs: { lang: 'math' },
+ },
+ // Matches HTML generated by Banzai::Filter::MermaidFilter,
+ // after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js
+ {
+ tag: 'svg.mermaid',
+ preserveWhitespace: 'full',
+ contentElement: 'text.source',
+ attrs: { lang: 'mermaid' },
+ },
+ // Matches HTML generated by Banzai::Filter::SuggestionFilter,
+ // after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+ {
+ tag: '.md-suggestion',
+ skip: true,
+ },
+ {
+ tag: '.md-suggestion-header',
+ ignore: true,
+ },
+ {
+ tag: '.md-suggestion-diff',
+ preserveWhitespace: 'full',
+ getContent: (el, schema) =>
+ [...el.querySelectorAll('.line_content.new span')].map(span =>
+ schema.text(span.innerText),
+ ),
+ attrs: { lang: 'suggestion' },
+ },
+ ],
+ toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]],
+ };
+ }
+
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ const {
+ textContent: text,
+ attrs: { lang },
+ } = node;
+
+ // Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks
+ if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) {
+ state.wrapBlock(' ', null, node, () => state.text(text, false));
+ return;
+ }
+
+ state.write('```');
+ if (lang !== PLAINTEXT_LANG) state.write(lang);
+
+ state.ensureNewLine();
+ state.text(text, false);
+ state.ensureNewLine();
+
+ state.write('```');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
new file mode 100644
index 00000000000..a4451d8ce8d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionDetails extends Node {
+ get name() {
+ return 'description_details';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'dd' }],
+ toDOM: () => ['dd', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.write('<dd>');
+ state.text(node.textContent, false);
+ state.write('</dd>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
new file mode 100644
index 00000000000..6aa1aca29d7
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionList extends Node {
+ get name() {
+ return 'description_list';
+ }
+
+ get schema() {
+ return {
+ content: '(description_term+ description_details+)+',
+ group: 'block',
+ parseDOM: [{ tag: 'dl' }],
+ toDOM: () => ['dl', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<dl>\n');
+ state.wrapBlock(' ', null, node, () => state.renderContent(node));
+ state.flushClose(1);
+ state.ensureNewLine();
+ state.write('</dl>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
new file mode 100644
index 00000000000..89057ec6444
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionTerm extends Node {
+ get name() {
+ return 'description_term';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'dt' }],
+ toDOM: () => ['dt', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2);
+ state.write('<dt>');
+ state.text(node.textContent, false);
+ state.write('</dt>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/details.js b/app/assets/javascripts/behaviors/markdown/nodes/details.js
new file mode 100644
index 00000000000..1c40dbb8168
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/details.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Details extends Node {
+ get name() {
+ return 'details';
+ }
+
+ get schema() {
+ return {
+ content: 'summary block*',
+ group: 'block',
+ parseDOM: [{ tag: 'details' }],
+ toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<details>\n');
+ state.renderContent(node);
+ state.flushClose(1);
+ state.ensureNewLine();
+ state.write('</details>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/doc.js b/app/assets/javascripts/behaviors/markdown/nodes/doc.js
new file mode 100644
index 00000000000..88b16fd85da
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/doc.js
@@ -0,0 +1,15 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+export default class Doc extends Node {
+ get name() {
+ return 'doc';
+ }
+
+ get schema() {
+ return {
+ content: 'block+',
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
new file mode 100644
index 00000000000..a7cc3e828f5
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter
+export default class Emoji extends Node {
+ get name() {
+ return 'emoji';
+ }
+
+ get schema() {
+ return {
+ inline: true,
+ group: 'inline',
+ attrs: {
+ name: {},
+ title: {},
+ moji: {},
+ },
+ parseDOM: [
+ {
+ tag: 'gl-emoji',
+ getAttrs: el => ({
+ name: el.dataset.name,
+ title: el.getAttribute('title'),
+ moji: el.textContent,
+ }),
+ },
+ ],
+ toDOM: node => [
+ 'gl-emoji',
+ { 'data-name': node.attrs.name, title: node.attrs.title },
+ node.attrs.moji,
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(`:${node.attrs.name}:`);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
new file mode 100644
index 00000000000..59e5d8ab3e2
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
@@ -0,0 +1,10 @@
+/* eslint-disable class-methods-use-this */
+
+import { HardBreak as BaseHardBreak } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class HardBreak extends BaseHardBreak {
+ toMarkdown(state) {
+ if (!state.atBlank()) state.write(' \n');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
new file mode 100644
index 00000000000..fec8608cf5d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
@@ -0,0 +1,13 @@
+/* eslint-disable class-methods-use-this */
+
+import { Heading as BaseHeading } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Heading extends BaseHeading {
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ defaultMarkdownSerializer.nodes.heading(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
new file mode 100644
index 00000000000..695c7160bde
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class HorizontalRule extends BaseHorizontalRule {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.horizontal_rule(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js
new file mode 100644
index 00000000000..c225a5ed876
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js
@@ -0,0 +1,52 @@
+/* eslint-disable class-methods-use-this */
+
+import { Image as BaseImage } from 'tiptap-extensions';
+import { placeholderImage } from '~/lazy_loader';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+export default class Image extends BaseImage {
+ get schema() {
+ return {
+ attrs: {
+ src: {},
+ alt: {
+ default: null,
+ },
+ title: {
+ default: null,
+ },
+ },
+ group: 'inline',
+ inline: true,
+ draggable: true,
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::ImageLinkFilter
+ {
+ tag: 'a.no-attachment-icon',
+ priority: 51,
+ skip: true,
+ },
+ // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
+ {
+ tag: 'img[src]',
+ getAttrs: el => {
+ const imageSrc = el.src;
+ const imageUrl =
+ imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
+
+ return {
+ src: imageUrl,
+ title: el.getAttribute('title'),
+ alt: el.getAttribute('alt'),
+ };
+ },
+ },
+ ],
+ toDOM: node => ['img', node.attrs],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.image(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
new file mode 100644
index 00000000000..4237637ed9a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { ListItem as BaseListItem } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class ListItem extends BaseListItem {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.list_item(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
new file mode 100644
index 00000000000..4c1542d14ea
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
@@ -0,0 +1,10 @@
+/* eslint-disable class-methods-use-this */
+
+import { OrderedList as BaseOrderedList } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class OrderedList extends BaseOrderedList {
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '1. ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
new file mode 100644
index 00000000000..25c4976a1bc
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class OrderedTaskList extends Node {
+ get name() {
+ return 'ordered_task_list';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ content: '(task_list_item|list_item)+',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'ol.task-list',
+ },
+ ],
+ toDOM: () => ['ol', { class: 'task-list' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '1. ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
new file mode 100644
index 00000000000..dec3207b1bb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Paragraph extends Node {
+ get name() {
+ return 'paragraph';
+ }
+
+ get schema() {
+ return {
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p' }],
+ toDOM: () => ['p', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.paragraph(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js
new file mode 100644
index 00000000000..5d6bbeca833
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/reference.js
@@ -0,0 +1,52 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
+export default class Reference extends Node {
+ get name() {
+ return 'reference';
+ }
+
+ get schema() {
+ return {
+ inline: true,
+ group: 'inline',
+ atom: true,
+ attrs: {
+ className: {},
+ referenceType: {},
+ originalText: { default: null },
+ href: {},
+ text: {},
+ },
+ parseDOM: [
+ {
+ tag: 'a.gfm:not([data-link=true])',
+ priority: 51,
+ getAttrs: el => ({
+ className: el.className,
+ referenceType: el.dataset.referenceType,
+ originalText: el.dataset.original,
+ href: el.getAttribute('href'),
+ text: el.textContent,
+ }),
+ },
+ ],
+ toDOM: node => [
+ 'a',
+ {
+ class: node.attrs.className,
+ href: node.attrs.href,
+ 'data-reference-type': node.attrs.referenceType,
+ 'data-original': node.attrs.originalText,
+ },
+ node.attrs.text,
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(node.attrs.originalText || node.attrs.text);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/summary.js b/app/assets/javascripts/behaviors/markdown/nodes/summary.js
new file mode 100644
index 00000000000..2e36e316d71
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/summary.js
@@ -0,0 +1,27 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Summary extends Node {
+ get name() {
+ return 'summary';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'summary' }],
+ toDOM: () => ['summary', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<summary>');
+ state.text(node.textContent, false);
+ state.write('</summary>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table.js b/app/assets/javascripts/behaviors/markdown/nodes/table.js
new file mode 100644
index 00000000000..a7fcb9227cd
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table.js
@@ -0,0 +1,25 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Table extends Node {
+ get name() {
+ return 'table';
+ }
+
+ get schema() {
+ return {
+ content: 'table_head table_body',
+ group: 'block',
+ isolating: true,
+ parseDOM: [{ tag: 'table' }],
+ toDOM: () => ['table', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
new file mode 100644
index 00000000000..403556dc0c8
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableBody extends Node {
+ get name() {
+ return 'table_body';
+ }
+
+ get schema() {
+ return {
+ content: 'table_row+',
+ parseDOM: [{ tag: 'tbody' }],
+ toDOM: () => ['tbody', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
new file mode 100644
index 00000000000..c63bfe10e39
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
@@ -0,0 +1,35 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableCell extends Node {
+ get name() {
+ return 'table_cell';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ header: { default: false },
+ align: { default: null },
+ },
+ content: 'inline*',
+ isolating: true,
+ parseDOM: [
+ {
+ tag: 'td, th',
+ getAttrs: el => ({
+ header: el.tagName === 'TH',
+ align: el.getAttribute('align') || el.style.textAlign,
+ }),
+ },
+ ],
+ toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderInline(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
new file mode 100644
index 00000000000..4cb94bf088c
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableHead extends Node {
+ get name() {
+ return 'table_head';
+ }
+
+ get schema() {
+ return {
+ content: 'table_header_row',
+ parseDOM: [{ tag: 'thead' }],
+ toDOM: () => ['thead', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
new file mode 100644
index 00000000000..e7eee636402
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
@@ -0,0 +1,43 @@
+/* eslint-disable class-methods-use-this */
+
+import TableRow from './table_row';
+
+const CENTER_ALIGN = 'center';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableHeaderRow extends TableRow {
+ get name() {
+ return 'table_header_row';
+ }
+
+ get schema() {
+ return {
+ content: 'table_cell+',
+ parseDOM: [
+ {
+ tag: 'thead tr',
+ priority: 51,
+ },
+ ],
+ toDOM: () => ['tr', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ const cellWidths = super.toMarkdown(state, node);
+
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
new file mode 100644
index 00000000000..20c7fa8a9ab
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
@@ -0,0 +1,33 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
+export default class TableOfContents extends Node {
+ get name() {
+ return 'table_of_contents';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ atom: true,
+ parseDOM: [
+ {
+ tag: 'ul.section-nav',
+ priority: 51,
+ },
+ {
+ tag: 'p.table-of-contents',
+ priority: 51,
+ },
+ ],
+ toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('[[_TOC_]]');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
new file mode 100644
index 00000000000..5852502773a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
@@ -0,0 +1,38 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableRow extends Node {
+ get name() {
+ return 'table_row';
+ }
+
+ get schema() {
+ return {
+ content: 'table_cell+',
+ parseDOM: [{ tag: 'tr' }],
+ toDOM: () => ['tr', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ const cellWidths = [];
+
+ state.flushClose(1);
+
+ state.write('| ');
+ node.forEach((cell, _, i) => {
+ if (i) state.write(' | ');
+
+ const { length } = state.out;
+ state.render(cell, node, i);
+ cellWidths.push(state.out.length - length);
+ });
+ state.write(' |');
+
+ state.closeBlock(node);
+
+ return cellWidths;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
new file mode 100644
index 00000000000..ab33bc21502
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class TaskList extends Node {
+ get name() {
+ return 'task_list';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ content: '(task_list_item|list_item)+',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'ul.task-list',
+ },
+ ],
+ toDOM: () => ['ul', { class: 'task-list' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '* ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
new file mode 100644
index 00000000000..d0ee7333d5e
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
@@ -0,0 +1,49 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class TaskListItem extends Node {
+ get name() {
+ return 'task_list_item';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ done: {
+ default: false,
+ },
+ },
+ defining: true,
+ draggable: false,
+ content: 'paragraph block*',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'li.task-list-item',
+ getAttrs: el => {
+ const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
+ return { done: checkbox && checkbox.checked };
+ },
+ },
+ ],
+ toDOM(node) {
+ return [
+ 'li',
+ { class: 'task-list-item' },
+ [
+ 'input',
+ { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done },
+ ],
+ ['div', { class: 'todo-content' }, 0],
+ ];
+ },
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(`[${node.attrs.done ? 'x' : ' '}] `);
+ state.renderContent(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js
new file mode 100644
index 00000000000..84838c14999
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/text.js
@@ -0,0 +1,20 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+export default class Text extends Node {
+ get name() {
+ return 'text';
+ }
+
+ get schema() {
+ return {
+ group: 'inline',
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.text(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/video.js b/app/assets/javascripts/behaviors/markdown/nodes/video.js
new file mode 100644
index 00000000000..516f983397d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/video.js
@@ -0,0 +1,54 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter
+export default class Video extends Node {
+ get name() {
+ return 'video';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ src: {},
+ alt: {
+ default: null,
+ },
+ },
+ group: 'block',
+ draggable: true,
+ parseDOM: [
+ {
+ tag: '.video-container',
+ skip: true,
+ },
+ {
+ tag: '.video-container p',
+ priority: 51,
+ ignore: true,
+ },
+ {
+ tag: 'video[src]',
+ getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
+ },
+ ],
+ toDOM: node => [
+ 'video',
+ {
+ src: node.attrs.src,
+ width: '400',
+ controls: true,
+ 'data-setup': '{}',
+ 'data-title': node.attrs.alt,
+ },
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.image(state, node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 35380ca49fb..798114b4b0b 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,4 +1,5 @@
import flash from '~/flash';
+import { sprintf, __ } from '../../locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
@@ -14,6 +15,9 @@ import flash from '~/flash';
// </pre>
//
+// This is an arbitary number; Can be iterated upon when suitable.
+const MAX_CHAR_LIMIT = 5000;
+
export default function renderMermaid($els) {
if (!$els.length) return;
@@ -34,6 +38,21 @@ export default function renderMermaid($els) {
$els.each((i, el) => {
const source = el.textContent;
+ /**
+ * Restrict the rendering to a certain amount of character to
+ * prevent mermaidjs from hanging up the entire thread and
+ * causing a DoS.
+ */
+ if (source && source.length > MAX_CHAR_LIMIT) {
+ el.textContent = sprintf(
+ __(
+ 'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
+ ),
+ { charLimit: MAX_CHAR_LIMIT },
+ );
+ return;
+ }
+
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });
diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js
new file mode 100644
index 00000000000..163182ab778
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/schema.js
@@ -0,0 +1,24 @@
+import { Schema } from 'prosemirror-model';
+import editorExtensions from './editor_extensions';
+
+const nodes = editorExtensions
+ .filter(extension => extension.type === 'node')
+ .reduce(
+ (ns, { name, schema }) => ({
+ ...ns,
+ [name]: schema,
+ }),
+ {},
+ );
+
+const marks = editorExtensions
+ .filter(extension => extension.type === 'mark')
+ .reduce(
+ (ms, { name, schema }) => ({
+ ...ms,
+ [name]: schema,
+ }),
+ {},
+ );
+
+export default new Schema({ nodes, marks });
diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js
new file mode 100644
index 00000000000..70dbd8bd206
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/serializer.js
@@ -0,0 +1,24 @@
+import { MarkdownSerializer } from 'prosemirror-markdown';
+import editorExtensions from './editor_extensions';
+
+const nodes = editorExtensions
+ .filter(extension => extension.type === 'node')
+ .reduce(
+ (ns, { name, toMarkdown }) => ({
+ ...ns,
+ [name]: toMarkdown,
+ }),
+ {},
+ );
+
+const marks = editorExtensions
+ .filter(extension => extension.type === 'mark')
+ .reduce(
+ (ms, { name, toMarkdown }) => ({
+ ...ms,
+ [name]: toMarkdown,
+ }),
+ {},
+ );
+
+export default new MarkdownSerializer(nodes, marks);
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 35f1bb6b080..7adccbb062f 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -28,16 +28,13 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function($form) {
var mdText;
- var markdownVersion;
- var url;
var preview = $form.find('.js-md-preview');
+ var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
- markdownVersion = $form.attr('data-markdown-version');
- url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) {
preview.text(this.emptyMessage);
@@ -67,16 +64,6 @@ MarkdownPreview.prototype.showPreview = function($form) {
}
};
-MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) {
- if (typeof markdownVersion === 'undefined') {
- return markdownPreviewPath;
- }
-
- return `${markdownPreviewPath}${
- markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
- }markdown_version=${markdownVersion}`;
-};
-
MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) {
if (!url) {
return;
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 2918e1486a7..680f2031409 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
-import _ from 'underscore';
import Sidebar from '../../right_sidebar';
import Shortcuts from './shortcuts';
import { CopyAsGFM } from '../markdown/copy_as_gfm';
@@ -63,28 +62,32 @@ export default class ShortcutsIssuable extends Shortcuts {
}
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- const selected = CopyAsGFM.nodeToGFM(el);
-
- if (selected.trim() === '') {
- return false;
- }
-
- const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
-
- // If replyField already has some content, add a newline before our quote
- const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
- $replyField
- .val((a, current) => `${current}${separator}${quote.join('')}\n`)
- .trigger('input')
- .trigger('change');
-
- // Trigger autosize
- const event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- $replyField.get(0).dispatchEvent(event);
+ const blockquoteEl = document.createElement('blockquote');
+ blockquoteEl.appendChild(el);
+ CopyAsGFM.nodeToGFM(blockquoteEl)
+ .then(text => {
+ if (text.trim() === '') {
+ return false;
+ }
+
+ // If replyField already has some content, add a newline before our quote
+ const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+ $replyField
+ .val((a, current) => `${current}${separator}${text}\n\n`)
+ .trigger('input')
+ .trigger('change');
+
+ // Trigger autosize
+ const event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ $replyField.get(0).dispatchEvent(event);
+
+ // Focus the input field
+ $replyField.focus();
- // Focus the input field
- $replyField.focus();
+ return false;
+ })
+ .catch(() => {});
return false;
}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b07f951346e..5f64175362d 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -16,6 +16,7 @@ export default () => {
const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
+ const isMarkdown = editBlobForm.data('is-markdown');
const commitButton = $('.js-commit-button');
const cancelLink = $('.btn.btn-cancel');
@@ -27,7 +28,13 @@ export default () => {
window.onbeforeunload = null;
});
- new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId);
+ new EditBlob({
+ assetsPath: `${urlRoot}${assetsPath}`,
+ filePath,
+ currentAction,
+ projectId,
+ isMarkdown,
+ });
new NewCommitForm(editBlobForm);
// returning here blocks page navigation
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 6e19548eed2..011898a5e7a 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -6,22 +6,31 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
+import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
export default class EditBlob {
- constructor(assetsPath, aceMode, currentAction, projectId) {
- this.configureAceEditor(aceMode, assetsPath);
+ // The options object has:
+ // assetsPath, filePath, currentAction, projectId, isMarkdown
+ constructor(options) {
+ this.options = options;
+ this.configureAceEditor();
this.initModePanesAndLinks();
this.initSoftWrap();
- this.initFileSelectors(currentAction, projectId);
+ this.initFileSelectors();
}
- configureAceEditor(filePath, assetsPath) {
+ configureAceEditor() {
+ const { filePath, assetsPath, isMarkdown } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor');
+ if (isMarkdown) {
+ addEditorMarkdownListeners(this.editor);
+ }
+
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
@@ -32,7 +41,8 @@ export default class EditBlob {
}
}
- initFileSelectors(currentAction, projectId) {
+ initFileSelectors() {
+ const { currentAction, projectId } = this.options;
this.fileTemplateMediator = new TemplateSelectorMediator({
currentAction,
editor: this.editor,
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 30fbdb9e97f..f569322ab70 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -86,7 +86,7 @@ export default {
class="board-card"
@mousedown="mouseDown"
@mousemove="mouseMove"
- @mouseup="showIssue($event);"
+ @mouseup="showIssue($event)"
>
<issue-card-inner
:list="list"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 93bcb4e129e..28d96dab605 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -96,7 +96,7 @@ export default {
<template>
<div class="board-new-issue-form">
<div class="board-card">
- <form @submit="submit($event);">
+ <form @submit="submit($event)">
<div v-if="error" class="flash-container">
<div class="flash-alert">An error occurred. Please try again.</div>
</div>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 0f581c3d37d..90ab3a76342 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -184,7 +184,7 @@ export default {
:title="label.description"
class="badge color-label append-right-4 prepend-top-4"
type="button"
- @click="filterByLabel(label);"
+ @click="filterByLabel(label)"
>
{{ label.title }}
</button>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index e038198e6f0..9bc66978198 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -3,7 +3,12 @@ import dateFormat from 'dateformat';
import { GlTooltip } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
-import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+import {
+ getDayDifference,
+ getTimeago,
+ dateInWords,
+ parsePikadayDate,
+} from '~/lib/utils/datetime_utility';
export default {
components: {
@@ -48,13 +53,13 @@ export default {
} else if (timeDifference === -1) {
return __('Yesterday');
} else if (timeDifference > 0 && timeDifference < 7) {
- return dateFormat(issueDueDate, 'dddd', true);
+ return dateFormat(issueDueDate, 'dddd');
}
return standardDateFormat;
},
issueDueDate() {
- return new Date(this.date);
+ return parsePikadayDate(this.date);
},
timeDifference() {
const today = new Date();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 08408eb0b52..2a0008467c4 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -45,7 +45,7 @@ export default {
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
- <aside class="svg-content"><img :src="emptyStateSvg" /></aside>
+ <aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
@@ -58,7 +58,7 @@ export default {
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
- @click="changeTab('all');"
+ @click="changeTab('all')"
>
Open issues
</button>
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index b1bc7d87086..d4afd9d59da 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -71,7 +71,7 @@ export default {
<span class="inline add-issues-footer-to-list"> to list </span>
<lists-dropdown />
</div>
- <button class="btn btn-default float-right" type="button" @click="toggleModal(false);">
+ <button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
Cancel
</button>
</footer>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index d0e285a149e..1f0961e02d8 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -58,7 +58,7 @@ export default {
class="close"
data-dismiss="modal"
aria-label="Close"
- @click="toggleModal(false);"
+ @click="toggleModal(false)"
>
<span aria-hidden="true">×</span>
</button>
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 878bb002c6c..e9ed2de859d 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -130,7 +130,7 @@ export default {
<div
:class="{ 'is-active': issue.selected }"
class="board-card"
- @click="toggleIssue($event, issue);"
+ @click="toggleIssue($event, issue)"
>
<issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
<icon
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 820d0679df5..3fbe8fe1be7 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -38,7 +38,7 @@ export default {
:class="{ 'is-active': list.id == selected.id }"
href="#"
role="button"
- @click.prevent="modal.selectedList = list;"
+ @click.prevent="modal.selectedList = list"
>
<span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span>
{{ list.title }}
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index 7b800a6ab97..2d2920e312e 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -21,12 +21,12 @@ export default {
<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');">
+ <a href="#" role="button" @click.prevent="changeTab('all')">
Open issues <span class="badge badge-pill"> {{ issuesCount }} </span>
</a>
</li>
<li :class="{ active: activeTab == 'selected' }">
- <a href="#" role="button" @click.prevent="changeTab('selected');">
+ <a href="#" role="button" @click.prevent="changeTab('selected')">
Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span>
</a>
</li>
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index d899b7fbd8c..8274647744f 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -82,7 +82,7 @@ export default {
<template>
<div>
<label class="label-bold prepend-top-10"> Project </label>
- <div ref="projectsDropdown" class="dropdown">
+ <div ref="projectsDropdown" class="dropdown dropdown-projects">
<button
class="dropdown-menu-toggle wide"
type="button"
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index f88e9b55988..c4c5fedc615 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -58,6 +58,7 @@ export default () => {
state: boardsStore.state,
loading: true,
boardsEndpoint: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId,
disabled: parseBoolean($boardApp.dataset.disabled),
@@ -75,6 +76,7 @@ export default () => {
created() {
gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
+ recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index dd3feedbc0e..9f6d9a853da 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -244,6 +244,7 @@ class List {
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
+ issue.assignableLabelsEndpoint = data.assignable_labels_endpoint;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 3de6eb056c2..7d463f17ab1 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,12 +2,13 @@ import axios from '../../lib/utils/axios_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
export default class BoardService {
- constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
+ constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
this.boardsEndpoint = boardsEndpoint;
this.boardId = boardId;
this.listsEndpoint = listsEndpoint;
this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.bulkUpdatePath = bulkUpdatePath;
+ this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`;
}
generateBoardsPath(id) {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index cf70a48f076..c59fac42038 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,12 +1,18 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
+import {
+ APPLICATION_STATUS,
+ REQUEST_SUBMITTED,
+ REQUEST_FAILURE,
+ UPGRADE_REQUESTED,
+ UPGRADE_REQUEST_FAILURE,
+} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
@@ -32,6 +38,7 @@ export default class Clusters {
installKnativePath,
installPrometheusPath,
managePrometheusPath,
+ hasRbac,
clusterType,
clusterStatus,
clusterStatusReason,
@@ -45,6 +52,7 @@ export default class Clusters {
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
+ this.store.updateRbac(hasRbac);
this.service = new ClustersService({
endpoint: statusPath,
installHelmEndpoint: installHelmPath,
@@ -67,7 +75,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
- initDismissableCallout('.js-cluster-security-warning');
+ Clusters.initDismissableCallout();
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(clusterType);
@@ -102,20 +110,32 @@ export default class Clusters {
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
+ rbac: this.state.rbac,
},
});
},
});
}
+ static initDismissableCallout() {
+ const callout = document.querySelector('.js-cluster-security-warning');
+ PersistentUserCallout.factory(callout);
+ }
+
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
+ eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
+ eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
+ eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
}
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
+ eventHub.$off('upgradeApplication', this.upgradeApplication);
+ eventHub.$off('upgradeFailed', this.upgradeFailed);
+ eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
}
initPolling() {
@@ -222,22 +242,33 @@ export default class Clusters {
installApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
this.store.updateAppProperty(appId, 'requestReason', null);
+ this.store.updateAppProperty(appId, 'statusReason', null);
+
+ this.service.installApplication(appId, data.params).catch(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin installing failed'),
+ );
+ });
+ }
+
+ upgradeApplication(data) {
+ const appId = data.id;
+ this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
+ this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
+ this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
+ }
+
+ upgradeFailed(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
+ }
- this.service
- .installApplication(appId, data.params)
- .then(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
- })
- .catch(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
- this.store.updateAppProperty(
- appId,
- 'requestReason',
- s__('ClusterIntegration|Request to begin installing failed'),
- );
- });
+ dismissUpgradeSuccess(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', null);
}
destroy() {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index d4354dcfebd..5952e93b9a7 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,20 +1,24 @@
<script>
/* eslint-disable vue/require-default-prop */
+import { GlLink } from '@gitlab/ui';
+import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
- REQUEST_LOADING,
- REQUEST_SUCCESS,
+ REQUEST_SUBMITTED,
REQUEST_FAILURE,
+ UPGRADE_REQUESTED,
} from '../constants';
export default {
components: {
loadingButton,
identicon,
+ TimeagoTooltip,
+ GlLink,
},
props: {
id: {
@@ -59,6 +63,18 @@ export default {
type: String,
required: false,
},
+ version: {
+ type: String,
+ required: false,
+ },
+ chartRepo: {
+ type: String,
+ required: false,
+ },
+ upgradeAvailable: {
+ type: Boolean,
+ required: false,
+ },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -72,11 +88,31 @@ export default {
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
+ isInstalling() {
+ return (
+ this.status === APPLICATION_STATUS.SCHEDULED ||
+ this.status === APPLICATION_STATUS.INSTALLING ||
+ (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled)
+ );
+ },
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
- this.status === APPLICATION_STATUS.UPDATING
+ this.status === APPLICATION_STATUS.UPDATING ||
+ this.status === APPLICATION_STATUS.UPDATE_ERRORED
+ );
+ },
+ canInstall() {
+ if (this.isInstalling) {
+ return false;
+ }
+
+ return (
+ this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
+ this.status === APPLICATION_STATUS.INSTALLABLE ||
+ this.status === APPLICATION_STATUS.ERROR ||
+ this.isUnknownStatus
);
},
hasLogo() {
@@ -90,12 +126,7 @@ export default {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
- return (
- !this.status ||
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING ||
- this.requestStatus === REQUEST_LOADING
- );
+ return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
@@ -104,30 +135,17 @@ export default {
return (
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
this.status !== APPLICATION_STATUS.ERROR) ||
- this.requestStatus === REQUEST_LOADING ||
- this.requestStatus === REQUEST_SUCCESS) &&
+ this.isInstalling) &&
this.isKnownStatus
);
},
installButtonLabel() {
let label;
- if (
- this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
- this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.ERROR ||
- this.isUnknownStatus
- ) {
+ if (this.canInstall) {
label = s__('ClusterIntegration|Install');
- } else if (
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING
- ) {
+ } else if (this.isInstalling) {
label = s__('ClusterIntegration|Installing');
- } else if (
- this.status === APPLICATION_STATUS.INSTALLED ||
- this.status === APPLICATION_STATUS.UPDATED ||
- this.status === APPLICATION_STATUS.UPDATING
- ) {
+ } else if (this.isInstalled) {
label = s__('ClusterIntegration|Installed');
}
@@ -140,13 +158,79 @@ export default {
return s__('ClusterIntegration|Manage');
},
hasError() {
- return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
+ return (
+ !this.isInstalling &&
+ (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
+ );
},
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
+ versionLabel() {
+ if (this.upgradeFailed) {
+ return s__('ClusterIntegration|Upgrade failed');
+ } else if (this.isUpgrading) {
+ return s__('ClusterIntegration|Upgrading');
+ }
+
+ return s__('ClusterIntegration|Upgraded');
+ },
+ upgradeRequested() {
+ return this.requestStatus === UPGRADE_REQUESTED;
+ },
+ upgradeSuccessful() {
+ return this.status === APPLICATION_STATUS.UPDATED;
+ },
+ upgradeFailed() {
+ if (this.isUpgrading) {
+ return false;
+ }
+
+ return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
+ },
+ upgradeFailureDescription() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
+ ),
+ {
+ title: this.title,
+ },
+ );
+ },
+ upgradeSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
+ title: this.title,
+ });
+ },
+ upgradeButtonLabel() {
+ let label;
+ if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
+ label = s__('ClusterIntegration|Upgrade');
+ } else if (this.isUpgrading) {
+ label = s__('ClusterIntegration|Upgrading');
+ } else if (this.upgradeFailed) {
+ label = s__('ClusterIntegration|Retry upgrade');
+ }
+
+ return label;
+ },
+ isUpgrading() {
+ // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
+ return (
+ this.status === APPLICATION_STATUS.UPDATING ||
+ (this.upgradeRequested && !this.upgradeSuccessful)
+ );
+ },
+ },
+ watch: {
+ status() {
+ if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
+ eventHub.$emit('upgradeFailed', this.id);
+ }
+ },
},
methods: {
installClicked() {
@@ -155,6 +239,15 @@ export default {
params: this.installApplicationRequestParams,
});
},
+ upgradeClicked() {
+ eventHub.$emit('upgradeApplication', {
+ id: this.id,
+ params: this.installApplicationRequestParams,
+ });
+ },
+ dismissUpgradeSuccess() {
+ eventHub.$emit('dismissUpgradeSuccess', this.id);
+ },
},
};
</script>
@@ -208,6 +301,51 @@ export default {
</li>
</ul>
</div>
+
+ <div
+ v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable"
+ class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
+ >
+ {{ versionLabel }}
+
+ <span v-if="upgradeSuccessful"> to</span>
+
+ <gl-link
+ v-if="upgradeSuccessful"
+ :href="chartRepo"
+ target="_blank"
+ class="js-cluster-application-upgrade-version"
+ >
+ chart v{{ version }}
+ </gl-link>
+ </div>
+
+ <div
+ v-if="upgradeFailed && !isUpgrading"
+ class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
+ >
+ {{ upgradeFailureDescription }}
+ </div>
+
+ <div
+ v-if="upgradeRequested && upgradeSuccessful"
+ class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
+ >
+ {{ upgradeSuccessDescription }}
+
+ <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
+ &times;
+ </button>
+ </div>
+
+ <loading-button
+ v-if="upgradeAvailable || upgradeFailed || isUpgrading"
+ class="btn btn-primary js-cluster-application-upgrade-button mt-2"
+ :loading="isUpgrading"
+ :disabled="isUpgrading"
+ :label="upgradeButtonLabel"
+ @click="upgradeClicked"
+ />
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 489615f1f78..0cf187d4189 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -52,6 +52,11 @@ export default {
required: false,
default: '',
},
+ rbac: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data: () => ({
elasticsearchLogo,
@@ -357,6 +362,9 @@ export default {
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
+ :version="applications.runner.version"
+ :chart-repo="applications.runner.chartRepo"
+ :upgrade-available="applications.runner.upgradeAvailable"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
@@ -442,6 +450,18 @@ export default {
title-link="https://github.com/knative/docs"
>
<div slot="description">
+ <span v-if="!rbac">
+ <p v-if="!rbac" class="bs-callout bs-callout-info append-bottom-0">
+ {{
+ s__(`ClusterIntegration|You must have an RBAC-enabled cluster
+ to install Knative.`)
+ }}
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+ <br />
+ </span>
<p>
{{
s__(`ClusterIntegration|Knative extends Kubernetes to provide
@@ -465,7 +485,7 @@ export default {
/>
</div>
</template>
- <template v-else-if="helmInstalled">
+ <template v-else-if="helmInstalled && rbac">
<div class="form-group">
<label for="knative-domainname">
{{ s__('ClusterIntegration|Knative Domain Name:') }}
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index e31afadf186..39022879d91 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -12,16 +12,19 @@ export const APPLICATION_STATUS = {
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
- UPDATED: 'updated',
UPDATING: 'updating',
+ UPDATED: 'updated',
+ UPDATE_ERRORED: 'update_errored',
ERROR: 'errored',
};
// These are only used client-side
-export const REQUEST_LOADING = 'request-loading';
-export const REQUEST_SUCCESS = 'request-success';
+export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure';
+export const UPGRADE_REQUESTED = 'upgrade-requested';
+export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
+export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index c750daab112..d309678be27 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,6 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
+import { parseBoolean } from '../../lib/utils/common_utils';
+import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants';
export default class ClusterStore {
constructor() {
@@ -7,6 +8,7 @@ export default class ClusterStore {
helpPath: null,
ingressHelpPath: null,
status: null,
+ rbac: false,
statusReason: null,
applications: {
helm: {
@@ -38,6 +40,9 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ version: null,
+ chartRepo: 'https://gitlab.com/charts/gitlab-runner',
+ upgradeAvailable: null,
},
prometheus: {
title: s__('ClusterIntegration|Prometheus'),
@@ -81,6 +86,10 @@ export default class ClusterStore {
this.state.status = status;
}
+ updateRbac(rbac) {
+ this.state.rbac = parseBoolean(rbac);
+ }
+
updateStatusReason(reason) {
this.state.statusReason = reason;
}
@@ -94,7 +103,13 @@ export default class ClusterStore {
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach(serverAppEntry => {
- const { name: appId, status, status_reason: statusReason } = serverAppEntry;
+ const {
+ name: appId,
+ status,
+ status_reason: statusReason,
+ version,
+ update_available: upgradeAvailable,
+ } = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),
@@ -118,6 +133,9 @@ export default class ClusterStore {
serverAppEntry.hostname || this.state.applications.knative.hostname;
this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
+ } else if (appId === RUNNER) {
+ this.state.applications.runner.version = version;
+ this.state.applications.runner.upgradeAvailable = upgradeAvailable;
}
});
}
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index a7ed175f7a4..009153d0703 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
-import 'select2/select2';
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 10f02739ec8..67fcdd082a2 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -4,6 +4,10 @@ import _ from 'underscore';
import bp from './breakpoints';
import { parseBoolean } from '~/lib/utils/common_utils';
+// NOTE: at 1200px nav sidebar should not overlap the content
+// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24555#note_134136110
+const NAV_SIDEBAR_BREAKPOINT = 1200;
+
export default class ContextualSidebar {
constructor() {
this.initDomElements();
@@ -13,6 +17,9 @@ export default class ContextualSidebar {
initDomElements() {
this.$page = $('.layout-page');
this.$sidebar = $('.nav-sidebar');
+
+ if (!this.$sidebar.length) return;
+
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
@@ -21,48 +28,63 @@ export default class ContextualSidebar {
}
bindEvents() {
- document.addEventListener('click', e => {
- if (
- !e.target.closest('.nav-sidebar') &&
- (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
- ) {
- this.toggleCollapsedSidebar(true);
- }
- });
+ if (!this.$sidebar.length) return;
+
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
this.$sidebarToggle.on('click', () => {
- const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
- this.toggleCollapsedSidebar(value);
+ if (!ContextualSidebar.isDesktopBreakpoint()) {
+ this.toggleSidebarNav(!this.$sidebar.hasClass('sidebar-expanded-mobile'));
+ } else {
+ const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
+ this.toggleCollapsedSidebar(value, true);
+ }
});
$(window).on('resize', () => _.debounce(this.render(), 100));
}
+ // TODO: use the breakpoints from breakpoints.js once they have been updated for bootstrap 4
+ // See documentation: https://design.gitlab.com/regions/navigation#contextual-navigation
+ static isDesktopBreakpoint = () => bp.windowWidth() >= NAV_SIDEBAR_BREAKPOINT;
static setCollapsedCookie(value) {
- if (bp.getBreakpointSize() !== 'lg') {
+ if (!ContextualSidebar.isDesktopBreakpoint()) {
return;
}
Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
}
toggleSidebarNav(show) {
- this.$sidebar.toggleClass('sidebar-expanded-mobile', show);
- this.$overlay.toggleClass('mobile-nav-open', show);
+ const breakpoint = bp.getBreakpointSize();
+ const dbp = ContextualSidebar.isDesktopBreakpoint();
+
+ this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
+ this.$overlay.toggleClass(
+ 'mobile-nav-open',
+ breakpoint === 'xs' || breakpoint === 'sm' ? show : false,
+ );
this.$sidebar.removeClass('sidebar-collapsed-desktop');
}
- toggleCollapsedSidebar(collapsed) {
+ toggleCollapsedSidebar(collapsed, saveCookie) {
const breakpoint = bp.getBreakpointSize();
+ const dbp = ContextualSidebar.isDesktopBreakpoint();
if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed);
- this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
+ this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
+ this.$page.toggleClass(
+ 'page-with-icon-sidebar',
+ breakpoint === 'xs' || breakpoint === 'sm' ? true : collapsed,
+ );
+ }
+
+ if (saveCookie) {
+ ContextualSidebar.setCollapsedCookie(collapsed);
}
- ContextualSidebar.setCollapsedCookie(collapsed);
- this.toggleSidebarOverflow();
+ requestIdleCallback(() => this.toggleSidebarOverflow());
}
toggleSidebarOverflow() {
@@ -74,13 +96,13 @@ export default class ContextualSidebar {
}
render() {
- const breakpoint = bp.getBreakpointSize();
+ if (!this.$sidebar.length) return;
- if (breakpoint === 'sm' || breakpoint === 'md') {
- this.toggleCollapsedSidebar(true);
- } else if (breakpoint === 'lg') {
+ if (!ContextualSidebar.isDesktopBreakpoint()) {
+ this.toggleSidebarNav(false);
+ } else {
const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
- this.toggleCollapsedSidebar(collapse);
+ this.toggleCollapsedSidebar(collapse, true);
}
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index f01e6f2a639..6ffb8c4e1c0 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -113,7 +113,7 @@ export default {
<div class="gl-responsive-table-row deploy-key">
<div class="table-section section-40">
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div>
- <div class="table-mobile-content">
+ <div class="table-mobile-content qa-key">
<strong class="title qa-key-title"> {{ deployKey.title }} </strong>
<div class="fingerprint qa-key-fingerprint">{{ deployKey.fingerprint }}</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index d4c1b07093d..1fc2b7fe859 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -4,6 +4,8 @@ import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import Mousetrap from 'mousetrap';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -11,6 +13,13 @@ import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
+import {
+ TREE_LIST_WIDTH_STORAGE_KEY,
+ INITIAL_TREE_WIDTH,
+ MIN_TREE_WIDTH,
+ MAX_TREE_WIDTH,
+ TREE_HIDE_STATS_WIDTH,
+} from '../constants';
export default {
name: 'DiffsApp',
@@ -23,6 +32,7 @@ export default {
CommitWidget,
TreeList,
GlLoadingIcon,
+ PanelResizer,
},
props: {
endpoint: {
@@ -54,8 +64,12 @@ export default {
},
},
data() {
+ const treeWidth =
+ parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH;
+
return {
assignedDiscussions: false,
+ treeWidth,
};
},
computed: {
@@ -74,7 +88,7 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
- ...mapGetters('diffs', ['isParallelView']),
+ ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
@@ -96,6 +110,9 @@ export default {
this.startVersion.version_index === this.mergeRequestDiff.version_index)
);
},
+ hideFileStats() {
+ return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
+ },
},
watch: {
diffViewType() {
@@ -129,6 +146,11 @@ export default {
created() {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
+ eventHub.$once('fetchDiffData', this.fetchData);
+ },
+ beforeDestroy() {
+ eventHub.$off('fetchDiffData', this.fetchData);
+ this.removeEventListeners();
},
methods: {
...mapActions(['startTaskList']),
@@ -138,6 +160,8 @@ export default {
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
'setHighlightedRow',
+ 'cacheTreeListWidth',
+ 'scrollToFile',
]),
fetchData() {
this.fetchDiffFiles()
@@ -176,10 +200,40 @@ export default {
this.$nextTick(() => {
window.mrTabs.resetViewContainer();
window.mrTabs.expandViewContainer(this.showTreeList);
+ this.setEventListeners();
});
+ } else {
+ this.removeEventListeners();
+ }
+ },
+ setEventListeners() {
+ Mousetrap.bind(['[', 'k', ']', 'j'], (e, combo) => {
+ switch (combo) {
+ case '[':
+ case 'k':
+ this.jumpToFile(-1);
+ break;
+ case ']':
+ case 'j':
+ this.jumpToFile(+1);
+ break;
+ default:
+ break;
+ }
+ });
+ },
+ removeEventListeners() {
+ Mousetrap.unbind(['[', 'k', ']', 'j']);
+ },
+ jumpToFile(step) {
+ const targetIndex = this.currentDiffIndex + step;
+ if (targetIndex >= 0 && targetIndex < this.diffFiles.length) {
+ this.scrollToFile(this.diffFiles[targetIndex].file_path);
}
},
},
+ minTreeWidth: MIN_TREE_WIDTH,
+ maxTreeWidth: MAX_TREE_WIDTH,
};
</script>
@@ -205,7 +259,21 @@ export default {
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default"
>
- <div v-show="showTreeList" class="diff-tree-list"><tree-list /></div>
+ <div
+ v-show="showTreeList"
+ :style="{ width: `${treeWidth}px` }"
+ class="diff-tree-list js-diff-tree-list"
+ >
+ <panel-resizer
+ :size.sync="treeWidth"
+ :start-size="treeWidth"
+ :min-size="$options.minTreeWidth"
+ :max-size="$options.maxTreeWidth"
+ side="right"
+ @resize-end="cacheTreeListWidth"
+ />
+ <tree-list :hide-file-stats="hideFileStats" />
+ </div>
<div class="diff-files-holder">
<commit-widget v-if="commit" :commit="commit" />
<template v-if="renderDiffFiles">
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index ebc4a83af4d..c02a8740a42 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -5,6 +5,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CIIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import initUserPopovers from '../../user_popovers';
/**
* CommitItem
@@ -35,20 +36,30 @@ export default {
},
},
computed: {
+ author() {
+ return this.commit.author || {};
+ },
authorName() {
- return (this.commit.author && this.commit.author.name) || this.commit.author_name;
+ return this.author.name || this.commit.author_name;
+ },
+ authorClass() {
+ return this.author.name ? 'js-user-link' : '';
+ },
+ authorId() {
+ return this.author.id ? this.author.id : '';
},
authorUrl() {
- return (
- (this.commit.author && this.commit.author.web_url) || `mailto:${this.commit.author_email}`
- );
+ return this.author.web_url || `mailto:${this.commit.author_email}`;
},
authorAvatar() {
- return (
- (this.commit.author && this.commit.author.avatar_url) || this.commit.author_gravatar_url
- );
+ return this.author.avatar_url || this.commit.author_gravatar_url;
},
},
+ created() {
+ this.$nextTick(() => {
+ initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
+ });
+ },
};
</script>
@@ -81,7 +92,13 @@ export default {
</button>
<div class="commiter">
- <a :href="authorUrl" v-text="authorName"></a> {{ s__('CommitWidget|authored') }}
+ <a
+ :href="authorUrl"
+ :class="authorClass"
+ :data-user-id="authorId"
+ v-text="authorName"
+ ></a>
+ {{ s__('CommitWidget|authored') }}
<time-ago-tooltip :time="commit.authored_date" />
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index f75345d31f8..0bf2dde8b96 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -2,9 +2,11 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
+import SettingsDropdown from './settings_dropdown.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -12,6 +14,8 @@ export default {
Icon,
GlLink,
GlButton,
+ SettingsDropdown,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -33,27 +37,33 @@ export default {
},
},
computed: {
- ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
+ ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
+ ...mapState('diffs', [
+ 'commit',
+ 'showTreeList',
+ 'startVersion',
+ 'latestVersionPath',
+ 'addedLines',
+ 'removedLines',
+ ]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
- toggleWhitespaceText() {
- if (this.isWhitespaceVisible()) {
- return __('Hide whitespace changes');
- }
- return __('Show whitespace changes');
- },
- toggleWhitespacePath() {
- if (this.isWhitespaceVisible()) {
- return mergeUrlParams({ w: 1 }, window.location.href);
- }
-
- return mergeUrlParams({ w: 0 }, window.location.href);
- },
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
+ fileTreeIcon() {
+ return this.showTreeList ? 'collapse-left' : 'expand-left';
+ },
+ toggleFileBrowserTitle() {
+ return this.showTreeList ? __('Hide file browser') : __('Show file browser');
+ },
+ baseVersionPath() {
+ return this.mergeRequestDiff.base_version_path;
+ },
+ },
+ mounted() {
+ polyfillSticky(this.$el);
},
methods: {
...mapActions('diffs', [
@@ -62,15 +72,12 @@ export default {
'expandAllFiles',
'toggleShowTreeList',
]),
- isWhitespaceVisible() {
- return getParameterValues('w')[0] !== '1';
- },
},
};
</script>
<template>
- <div class="mr-version-controls">
+ <div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }">
<div class="mr-version-menus-container content-block">
<button
v-gl-tooltip.hover
@@ -79,10 +86,10 @@ export default {
:class="{
active: showTreeList,
}"
- :title="__('Toggle file browser')"
+ :title="toggleFileBrowserTitle"
@click="toggleShowTreeList"
>
- <icon name="hamburger" />
+ <icon :name="fileTreeIcon" />
</button>
<div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container">
Changes between
@@ -95,6 +102,7 @@ export default {
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
+ :base-version-path="baseVersionPath"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
@@ -105,6 +113,11 @@ export default {
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
</div>
<div class="inline-parallel-buttons d-none d-md-flex ml-auto">
+ <diff-stats
+ :diff-files-length="diffFilesLength"
+ :added-lines="addedLines"
+ :removed-lines="removedLines"
+ />
<gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
@@ -115,31 +128,7 @@ export default {
<a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles">
{{ __('Expand all') }}
</a>
- <a :href="toggleWhitespacePath" class="btn btn-default qa-toggle-whitespace">
- {{ toggleWhitespaceText }}
- </a>
- <div class="btn-group prepend-left-8">
- <button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- type="button"
- class="btn js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </button>
- <button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- type="button"
- class="btn js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </button>
- </div>
+ <settings-dropdown />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 8da02ed0b7c..80aec84f574 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -34,14 +34,13 @@ export default {
required: false,
default: false,
},
+ baseVersionPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
- baseVersion() {
- return {
- name: 'hii',
- versionIndex: -1,
- };
- },
targetVersions() {
if (this.mergeRequestVersion) {
return this.otherVersions;
@@ -62,6 +61,9 @@ export default {
);
},
href(version) {
+ if (this.isBase(version)) {
+ return this.baseVersionPath;
+ }
if (this.showCommitCount) {
return version.version_path;
}
@@ -129,7 +131,7 @@ export default {
</strong>
</div>
<div>
- <small class="commit-sha"> {{ version.truncated_commit_sha }} </small>
+ <small class="commit-sha"> {{ version.short_commit_sha }} </small>
</div>
<div>
<small>
@@ -139,7 +141,7 @@ export default {
<time-ago
v-if="version.created_at"
:time="version.created_at"
- class="js-timeago js-timeago-render"
+ class="js-timeago"
/>
</small>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 42d09e44768..cb92093db32 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,7 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue';
+import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
+import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
import NoteForm from '../../notes/components/note_form.vue';
@@ -9,6 +10,7 @@ import ImageDiffOverlay from './image_diff_overlay.vue';
import DiffDiscussions from './diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils';
+import { diffViewerModes } from '~/ide/constants';
export default {
components: {
@@ -18,7 +20,8 @@ export default {
NoteForm,
DiffDiscussions,
ImageDiffOverlay,
- EmptyFileViewer,
+ NotDiffableViewer,
+ NoPreviewViewer,
},
props: {
diffFile: {
@@ -42,8 +45,17 @@ export default {
diffMode() {
return getDiffMode(this.diffFile);
},
+ diffViewerMode() {
+ return this.diffFile.viewer.name;
+ },
isTextFile() {
- return this.diffFile.viewer.name === 'text';
+ return this.diffViewerMode === diffViewerModes.text;
+ },
+ noPreview() {
+ return this.diffViewerMode === diffViewerModes.no_preview;
+ },
+ notDiffable() {
+ return this.diffViewerMode === diffViewerModes.not_diffable;
},
diffFileCommentForm() {
return this.getCommentFormForDiffFile(this.diffFile.file_hash);
@@ -77,9 +89,8 @@ export default {
<div class="diff-content">
<div class="diff-viewer">
<template v-if="isTextFile">
- <empty-file-viewer v-if="diffFile.empty" />
<inline-diff-view
- v-else-if="isInlineView"
+ v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines || []"
:help-page-path="helpPagePath"
@@ -91,9 +102,12 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
+ <not-diffable-viewer v-else-if="notDiffable" />
+ <no-preview-viewer v-else-if="noPreview" />
<diff-viewer
v-else
:diff-mode="diffMode"
+ :diff-viewer-mode="diffViewerMode"
:new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha"
:old-path="diffFile.old_path"
@@ -124,7 +138,7 @@ export default {
:save-button-title="__('Comment')"
class="diff-comment-form new-note discussion-form discussion-form-container"
@handleFormUpdate="handleSaveNote"
- @cancelForm="closeDiffFileCommentForm(diffFile.file_hash);"
+ @cancelForm="closeDiffFileCommentForm(diffFile.file_hash)"
/>
</div>
</diff-viewer>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index b2021cd6061..4c73eea4049 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -68,7 +68,7 @@ export default {
}"
type="button"
class="js-diff-notes-toggle"
- @click="toggleDiscussion({ discussionId: discussion.id });"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
>
<icon v-if="discussion.expanded" name="collapse" class="collapse-icon" />
<template v-else>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 449f7007077..1141a197c6a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -7,6 +7,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
+import { diffViewerErrors } from '~/ide/constants';
export default {
components: {
@@ -33,15 +34,13 @@ export default {
return {
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
+ isCollapsed: this.file.viewer.collapsed || false,
};
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']),
- isCollapsed() {
- return this.file.collapsed || false;
- },
viewBlobLink() {
return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
@@ -52,17 +51,6 @@ export default {
false,
);
},
- showExpandMessage() {
- return (
- this.isCollapsed ||
- (!this.file.highlighted_diff_lines &&
- !this.isLoadingCollapsedDiff &&
- !this.file.too_large &&
- this.file.text &&
- !this.file.renamed_file &&
- !this.file.mode_changed)
- );
- },
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
@@ -73,9 +61,15 @@ export default {
this.file.parallel_diff_lines.length > 0
);
},
+ isFileTooLarge() {
+ return this.file.viewer.error === diffViewerErrors.too_large;
+ },
+ errorMessage() {
+ return this.file.viewer.error_message;
+ },
},
watch: {
- 'file.collapsed': function fileCollapsedWatch(newVal, oldVal) {
+ isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
if (!newVal && oldVal && !this.hasDiffLines) {
this.handleLoadCollapsedDiff();
}
@@ -85,13 +79,13 @@ export default {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
},
methods: {
- ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
+ ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff', 'setRenderIt']),
handleToggle() {
if (!this.hasDiffLines) {
this.handleLoadCollapsedDiff();
} else {
- this.file.collapsed = !this.file.collapsed;
- this.file.renderIt = true;
+ this.isCollapsed = !this.isCollapsed;
+ this.setRenderIt(this.file);
}
},
handleLoadCollapsedDiff() {
@@ -100,8 +94,8 @@ export default {
this.loadCollapsedDiff(this.file)
.then(() => {
this.isLoadingCollapsedDiff = false;
- this.file.collapsed = false;
- this.file.renderIt = true;
+ this.isCollapsed = false;
+ this.setRenderIt(this.file);
})
.then(() => {
requestIdleCallback(
@@ -164,21 +158,25 @@ export default {
Cancel
</button>
</div>
-
- <diff-content
- v-if="!isCollapsed && file.renderIt"
- :class="{ hidden: isCollapsed || file.too_large }"
- :diff-file="file"
- :help-page-path="helpPagePath"
- />
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
- <div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed">
- {{ __('This diff is collapsed.') }}
- <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
- __('Click to expand it.')
- }}</a>
- </div>
- <div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff">
+ <template v-else>
+ <div v-if="errorMessage" class="diff-viewer">
+ <div class="nothing-here-block" v-html="errorMessage"></div>
+ </div>
+ <div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed">
+ {{ __('This diff is collapsed.') }}
+ <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
+ __('Click to expand it.')
+ }}</a>
+ </div>
+ <diff-content
+ v-else
+ :class="{ hidden: isCollapsed || isFileTooLarge }"
+ :diff-file="file"
+ :help-page-path="helpPagePath"
+ />
+ </template>
+ <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff">
{{ __('This source diff could not be displayed because it is too large.') }}
<span v-html="viewBlobLink"></span>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index f75a01b023b..4d33ad23f39 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -8,7 +8,9 @@ import FileIcon from '~/vue_shared/components/file_icon.vue';
import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
+import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -16,6 +18,7 @@ export default {
EditButton,
Icon,
FileIcon,
+ DiffStats,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -116,6 +119,12 @@ export default {
gfmCopyText() {
return `\`${this.diffFile.file_path}\``;
},
+ isFileRenamed() {
+ return this.diffFile.viewer.name === diffViewerModes.renamed;
+ },
+ isModeChanged() {
+ return this.diffFile.viewer.name === diffViewerModes.mode_changed;
+ },
},
mounted() {
polyfillSticky(this.$refs.header);
@@ -145,7 +154,7 @@ export default {
<div
ref="header"
class="js-file-title file-title file-title-flex-parent"
- @click="handleToggleFile($event, true);"
+ @click="handleToggleFile($event, true)"
>
<div class="file-header-content">
<icon
@@ -163,7 +172,7 @@ export default {
aria-hidden="true"
css-classes="js-file-icon append-right-5"
/>
- <span v-if="diffFile.renamed_file">
+ <span v-if="isFileRenamed">
<strong
v-gl-tooltip
:title="diffFile.old_path"
@@ -191,7 +200,7 @@ export default {
css-class="btn-default btn-transparent btn-clipboard"
/>
- <small v-if="diffFile.mode_changed" ref="fileMode">
+ <small v-if="isModeChanged" ref="fileMode">
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
</small>
@@ -202,6 +211,7 @@ export default {
v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-sm-block"
>
+ <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<template v-if="diffFile.blob && diffFile.blob.readable_text">
<button
:disabled="!diffHasDiscussions(diffFile)"
@@ -226,11 +236,15 @@ export default {
<a
v-if="diffFile.replaced_view_path"
:href="diffFile.replaced_view_path"
- class="btn view-file js-view-file"
+ class="btn view-file js-view-replaced-file"
v-html="viewReplacedFileButtonText"
>
</a>
- <a :href="diffFile.view_path" class="btn view-file js-view-file" v-html="viewFileButtonText">
+ <a
+ :href="diffFile.view_path"
+ class="btn view-file js-view-file-button"
+ v-html="viewFileButtonText"
+ >
</a>
<a
@@ -240,7 +254,7 @@ export default {
:title="`View on ${diffFile.formatted_external_url}`"
target="_blank"
rel="noopener noreferrer"
- class="btn btn-file-option"
+ class="btn btn-file-option js-external-url"
>
<icon name="external-link" />
</a>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index c0613d80d37..6709df48637 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -179,7 +179,7 @@ export default {
v-if="lineNumber"
:data-linenumber="lineNumber"
:href="lineHref"
- @click="setHighlightedRow(lineCode);"
+ @click="setHighlightedRow(lineCode)"
>
</a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index e7569ba7b84..bb66ab36283 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
+import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import noteForm from '../../notes/components/note_form.vue';
import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants';
@@ -9,7 +10,7 @@ export default {
components: {
noteForm,
},
- mixins: [autosave],
+ mixins: [autosave, diffLineNoteFormMixin],
props: {
diffFileHash: {
type: String,
@@ -28,6 +29,11 @@ export default {
type: Object,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState({
@@ -95,8 +101,10 @@ export default {
:is-editing="true"
:line-code="line.line_code"
:line="line"
+ :help-page-path="helpPagePath"
save-button-title="Comment"
class="diff-comment-form"
+ @handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
new file mode 100644
index 00000000000..2e5855380af
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -0,0 +1,52 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { n__ } from '~/locale';
+
+export default {
+ components: { Icon },
+ props: {
+ addedLines: {
+ type: Number,
+ required: true,
+ },
+ removedLines: {
+ type: Number,
+ required: true,
+ },
+ diffFilesLength: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ filesText() {
+ return n__('File', 'Files', this.diffFilesLength);
+ },
+ isCompareVersionsHeader() {
+ return Boolean(this.diffFilesLength);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="diff-stats"
+ :class="{
+ 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
+ 'd-inline-flex': !isCompareVersionsHeader,
+ }"
+ >
+ <div v-if="diffFilesLength !== null" class="diff-stats-group">
+ <icon name="doc-code" class="diff-stats-icon text-secondary" />
+ <strong>{{ diffFilesLength }} {{ filesText }}</strong>
+ </div>
+ <div class="diff-stats-group cgreen">
+ <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong>
+ </div>
+ <div class="diff-stats-group cred">
+ <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index 5d38d545ce8..803f23b9170 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -17,12 +17,7 @@ export default {
},
methods: {
handleEditClick(evt) {
- if (!this.canCurrentUserFork || this.canModifyBlob) {
- // if we can Edit, do default Edit button behavior
- return;
- }
-
- if (this.canCurrentUserFork) {
+ if (this.canCurrentUserFork && !this.canModifyBlob) {
evt.preventDefault();
this.$emit('showForkMessage');
}
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index d30e64312aa..4a83c5a72a5 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -97,7 +97,7 @@ export default {
v-if="canComment"
type="button"
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
- @click="clickedImage($event.offsetX, $event.offsetY);"
+ @click="clickedImage($event.offsetX, $event.offsetY)"
>
<span class="sr-only"> {{ __('Add image comment') }} </span>
</button>
@@ -109,7 +109,7 @@ export default {
:disabled="!shouldToggleDiscussion"
class="js-image-badge"
type="button"
- @click="toggleDiscussion({ discussionId: discussion.id });"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
>
<icon v-if="showCommentIcon" name="image-comment-dark" />
<template v-else>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 814ee0b7c02..69146f1f6fd 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -54,6 +54,7 @@ export default {
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
+ :help-page-path="helpPagePath"
/>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index e781397214d..8c76a555b62 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters } from 'vuex';
+import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
@@ -7,7 +8,10 @@ export default {
components: {
inlineDiffCommentRow,
inlineDiffTableRow,
+ InlineDraftCommentRow: () =>
+ import('ee_component/batch_comments/components/inline_draft_comment_row.vue'),
},
+ mixins: [draftCommentsMixin],
props: {
diffFile: {
type: Object,
@@ -54,6 +58,11 @@ export default {
:line="line"
:help-page-path="helpPagePath"
/>
+ <inline-draft-comment-row
+ v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
+ :key="`draft_${index}`"
+ :draft="draftForLine(diffFile.file_hash, line)"
+ />
</template>
</tbody>
</table>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index a65cf025cde..370cb6e339a 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -101,6 +101,7 @@ export default {
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
+ :help-page-path="helpPagePath"
line-position="left"
/>
</td>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 1bf693380db..93e754fa896 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters } from 'vuex';
+import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
@@ -7,7 +8,10 @@ export default {
components: {
parallelDiffTableRow,
parallelDiffCommentRow,
+ ParallelDraftCommentRow: () =>
+ import('ee_component/batch_comments/components/parallel_draft_comment_row.vue'),
},
+ mixins: [draftCommentsMixin],
props: {
diffFile: {
type: Object,
@@ -56,6 +60,12 @@ export default {
:line-index="index"
:help-page-path="helpPagePath"
/>
+ <parallel-draft-comment-row
+ v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
+ :key="`drafts-${index}`"
+ :line="line"
+ :diff-file-content-sha="diffFile.file_hash"
+ />
</template>
</tbody>
</table>
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
new file mode 100644
index 00000000000..0129763161a
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -0,0 +1,92 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ GlButton,
+ Icon,
+ },
+ computed: {
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ ...mapState('diffs', ['renderTreeList', 'showWhitespace']),
+ },
+ methods: {
+ ...mapActions('diffs', [
+ 'setInlineDiffViewType',
+ 'setParallelDiffViewType',
+ 'setRenderTreeList',
+ 'setShowWhitespace',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-default js-show-diff-settings"
+ data-toggle="dropdown"
+ data-display="static"
+ >
+ <icon name="settings" /> <icon name="arrow-down" />
+ </button>
+ <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
+ <div>
+ <span class="bold d-block mb-1">{{ __('File browser') }}</span>
+ <div class="btn-group d-flex">
+ <gl-button
+ :class="{ active: !renderTreeList }"
+ class="w-100 js-list-view"
+ @click="setRenderTreeList(false)"
+ >
+ {{ __('List view') }}
+ </gl-button>
+ <gl-button
+ :class="{ active: renderTreeList }"
+ class="w-100 js-tree-view"
+ @click="setRenderTreeList(true)"
+ >
+ {{ __('Tree view') }}
+ </gl-button>
+ </div>
+ </div>
+ <div class="mt-2">
+ <span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
+ <div class="btn-group d-flex js-diff-view-buttons">
+ <gl-button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ class="w-100 js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </gl-button>
+ <gl-button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ class="w-100 js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </gl-button>
+ </div>
+ </div>
+ <div class="mt-2">
+ <label class="mb-0">
+ <input
+ id="show-whitespace"
+ type="checkbox"
+ :checked="showWhitespace"
+ @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
+ />
+ {{ __('Show whitespace changes') }}
+ </label>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index f40a7b25fde..8fc3af15bea 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,13 +1,10 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
-const treeListStorageKey = 'mr_diff_tree_list';
-
export default {
directives: {
GlTooltip: GlTooltipDirective,
@@ -16,54 +13,51 @@ export default {
Icon,
FileRow,
},
+ props: {
+ hideFileStats: {
+ type: Boolean,
+ required: true,
+ },
+ },
data() {
- const treeListStored = localStorage.getItem(treeListStorageKey);
- const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
-
return {
search: '',
- renderTreeList,
- focusSearch: false,
};
},
computed: {
- ...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
- ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
+ ...mapState('diffs', ['tree', 'renderTreeList']),
+ ...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
- if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
+ if (search === '' || this.$options.fuzzyFileFinderEnabled)
+ return this.renderTreeList ? this.tree : this.allBlobs;
- return this.allBlobs.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
- },
- rowDisplayTextKey() {
- if (this.renderTreeList && this.search.trim() === '') {
- return 'name';
- }
+ return this.allBlobs.reduce((acc, folder) => {
+ const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
- return 'path';
+ if (tree.length) {
+ return acc.concat({
+ ...folder,
+ tree,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ fileRowExtraComponent() {
+ return this.hideFileStats ? null : FileRowStats;
},
},
methods: {
- ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
+ ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
clearSearch() {
this.search = '';
- this.toggleFocusSearch(false);
- },
- toggleRenderTreeList(toggle) {
- this.renderTreeList = toggle;
- localStorage.setItem(treeListStorageKey, this.renderTreeList);
- },
- toggleFocusSearch(toggle) {
- this.focusSearch = toggle;
- },
- blurSearch() {
- if (this.search.trim() === '') {
- this.toggleFocusSearch(false);
- }
},
},
- FileRowStats,
+ shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
+ diffTreeFiltering: gon.features && gon.features.diffTreeFiltering,
};
</script>
@@ -72,54 +66,39 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" />
- <input
- v-model="search"
- :placeholder="s__('MergeRequest|Filter files')"
- type="search"
- class="form-control"
- @focus="toggleFocusSearch(true);"
- @blur="blurSearch"
- />
- <button
- v-show="search"
- :aria-label="__('Clear search')"
- type="button"
- class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
- @click="clearSearch"
- >
- <icon name="close" />
- </button>
- </div>
- <div v-show="!focusSearch" class="btn-group prepend-left-8 tree-list-view-toggle">
- <button
- v-gl-tooltip.hover
- :aria-label="__('List view')"
- :title="__('List view')"
- :class="{
- active: !renderTreeList,
- }"
- class="btn btn-default pt-0 pb-0 d-flex align-items-center"
- type="button"
- @click="toggleRenderTreeList(false);"
- >
- <icon name="hamburger" />
- </button>
- <button
- v-gl-tooltip.hover
- :aria-label="__('Tree view')"
- :title="__('Tree view')"
- :class="{
- active: renderTreeList,
- }"
- class="btn btn-default pt-0 pb-0 d-flex align-items-center"
- type="button"
- @click="toggleRenderTreeList(true);"
- >
- <icon name="file-tree" />
- </button>
+ <template v-if="$options.diffTreeFiltering">
+ <input
+ v-model="search"
+ :placeholder="s__('MergeRequest|Filter files')"
+ type="search"
+ class="form-control"
+ />
+ <button
+ v-show="search"
+ :aria-label="__('Clear search')"
+ type="button"
+ class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
+ @click="clearSearch"
+ >
+ <icon name="close" />
+ </button>
+ </template>
+ <template v-else>
+ <button
+ type="button"
+ class="form-control text-left text-secondary"
+ @click="toggleFileFinder(true)"
+ >
+ {{ s__('MergeRequest|Search files') }}
+ </button>
+ <span
+ class="position-absolute text-secondary diff-tree-search-shortcut"
+ v-html="$options.shortcutKeyCharacter"
+ ></span>
+ </template>
</div>
</div>
- <div class="tree-list-scroll">
+ <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
<template v-if="filteredTreeList.length">
<file-row
v-for="file in filteredTreeList"
@@ -127,10 +106,8 @@ export default {
:file="file"
:level="0"
:hide-extra-on-tree="true"
- :extra-component="$options.FileRowStats"
+ :extra-component="fileRowExtraComponent"
:show-changed-icon="true"
- :display-text-key="rowDisplayTextKey"
- :should-truncate-start="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
@@ -139,12 +116,22 @@ export default {
{{ s__('MergeRequest|No files found') }}
</p>
</div>
- <div v-once class="pt-3 pb-3 text-center">
- {{ n__('%d changed file', '%d changed files', diffFilesLength) }}
- <div>
- <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span>
- <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span>
- </div>
- </div>
</div>
</template>
+
+<style>
+.tree-list-blobs .file-row-name {
+ margin-left: 12px;
+}
+
+.diff-tree-search-shortcut {
+ top: 50%;
+ right: 10px;
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+.tree-list-icon:not(button) {
+ pointer-events: none;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 78a39baa4cb..7002655ea49 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -32,3 +32,13 @@ export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
+
+export const TREE_TYPE = 'tree';
+export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
+export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
+export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width';
+
+export const INITIAL_TREE_WIDTH = 320;
+export const MIN_TREE_WIDTH = 240;
+export const MAX_TREE_WIDTH = 400;
+export const TREE_HIDE_STATS_WIDTH = 260;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index b130cedc24c..63954d9d412 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,8 +1,60 @@
import Vue from 'vue';
-import { mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import FindFile from '~/vue_shared/components/file_finder/index.vue';
+import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
+import { TREE_LIST_STORAGE_KEY } from './constants';
export default function initDiffsApp(store) {
+ const fileFinderEl = document.getElementById('js-diff-file-finder');
+
+ if (fileFinderEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: fileFinderEl,
+ store,
+ computed: {
+ ...mapState('diffs', ['fileFinderVisible', 'isLoading']),
+ ...mapGetters('diffs', ['flatBlobsList']),
+ },
+ watch: {
+ fileFinderVisible(newVal, oldVal) {
+ if (newVal && !oldVal && !this.flatBlobsList.length) {
+ eventHub.$emit('fetchDiffData');
+ }
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
+ openFile(file) {
+ window.mrTabs.tabShown('diffs');
+ this.scrollToFile(file.path);
+ },
+ },
+ render(createElement) {
+ return createElement(FindFile, {
+ props: {
+ files: this.flatBlobsList,
+ visible: this.fileFinderVisible,
+ loading: this.isLoading,
+ showDiffStats: true,
+ clearSearchOnClose: false,
+ },
+ on: {
+ toggle: this.toggleFileFinder,
+ click: this.openFile,
+ },
+ class: ['diff-file-finder'],
+ style: {
+ display: this.fileFinderVisible ? '' : 'none',
+ },
+ });
+ },
+ });
+ }
+
return new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
@@ -26,6 +78,16 @@ export default function initDiffsApp(store) {
activeTab: state => state.page.activeTab,
}),
},
+ created() {
+ const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY);
+ const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
+
+ this.setRenderTreeList(renderTreeList);
+ this.setShowWhitespace({ showWhitespace: getParameterValues('w')[0] !== '1' });
+ },
+ methods: {
+ ...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']),
+ },
render(createElement) {
return createElement('diffs-app', {
props: {
diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js
new file mode 100644
index 00000000000..cfa722b27f1
--- /dev/null
+++ b/app/assets/javascripts/diffs/mixins/draft_comments.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ shouldRenderDraftRow: () => () => false,
+ shouldRenderParallelDraftRow: () => () => false,
+ draftForLine: () => () => ({}),
+ },
+};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 00a4bb6d3a3..c40775c3259 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,6 +5,7 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
+import TreeWorker from '../workers/tree_worker';
import eventHub from '../../notes/event_hub';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types';
@@ -13,7 +14,11 @@ import {
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
+ TREE_LIST_STORAGE_KEY,
+ WHITESPACE_STORAGE_KEY,
+ TREE_LIST_WIDTH_STORAGE_KEY,
} from '../constants';
+import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => {
const { endpoint, projectPath } = options;
@@ -21,21 +26,35 @@ export const setBaseConfig = ({ commit }, options) => {
};
export const fetchDiffFiles = ({ state, commit }) => {
+ const worker = new TreeWorker();
+
commit(types.SET_LOADING, true);
+ worker.addEventListener('message', ({ data }) => {
+ commit(types.SET_TREE_DATA, data);
+
+ worker.terminate();
+ });
+
return axios
- .get(state.endpoint)
+ .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } })
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
commit(types.SET_DIFF_DATA, res.data);
+
+ worker.postMessage(state.diffFiles);
+
return Vue.nextTick();
})
- .then(handleLocationHash);
+ .then(handleLocationHash)
+ .catch(() => worker.terminate());
};
export const setHighlightedRow = ({ commit }, lineCode) => {
+ const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
+ commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};
// This is adding line discussions to the actual lines in the diff tree
@@ -76,7 +95,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
commit(types.RENDER_FILE, file);
}
- if (file.collapsed) {
+ if (file.viewer.collapsed) {
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
scrollToElement(document.getElementById(file.file_hash));
} else {
@@ -90,7 +109,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
const checkItem = () =>
new Promise(resolve => {
const nextFile = state.diffFiles.find(
- file => !file.renderIt && (!file.collapsed || !file.text),
+ file =>
+ !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text),
);
if (nextFile) {
@@ -113,6 +133,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
return checkItem();
};
+export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file);
+
export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
@@ -242,8 +264,6 @@ export const scrollToFile = ({ state, commit }, path) => {
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
-
- setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
};
export const toggleShowTreeList = ({ commit, state }) => {
@@ -265,5 +285,29 @@ export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
};
+export const setRenderTreeList = ({ commit }, renderTreeList) => {
+ commit(types.SET_RENDER_TREE_LIST, renderTreeList);
+
+ localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList);
+};
+
+export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => {
+ commit(types.SET_SHOW_WHITESPACE, showWhitespace);
+
+ localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace);
+
+ if (pushState) {
+ historyPushState(showWhitespace ? '?w=0' : '?w=1');
+ }
+};
+
+export const toggleFileFinder = ({ commit }, visible) => {
+ commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
+};
+
+export const cacheTreeListWidth = (_, size) => {
+ localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index fdf1efbb10e..bc27e263bff 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -4,7 +4,8 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
-export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed);
+export const hasCollapsedFile = state =>
+ state.diffFiles.some(file => file.viewer && file.viewer.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
@@ -74,12 +75,37 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
-export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
+export const flatBlobsList = state =>
+ Object.values(state.treeEntries).filter(f => f.type === 'blob');
+
+export const allBlobs = (state, getters) =>
+ getters.flatBlobsList.reduce((acc, file) => {
+ const { parentPath } = file;
+
+ if (parentPath && !acc.some(f => f.path === parentPath)) {
+ acc.push({
+ path: parentPath,
+ isHeader: true,
+ tree: [],
+ });
+ }
+
+ acc.find(f => f.path === parentPath).tree.push(file);
+
+ return acc;
+ }, []);
export const diffFilesLength = state => state.diffFiles.length;
export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash);
+/**
+ * Returns index of a currently selected diff in diffFiles
+ * @returns {number}
+ */
+export const currentDiffIndex = state =>
+ Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 98e57d52d77..47f78a5db54 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
+ addedLines: null,
+ removedLines: null,
endpoint: '',
basePath: '',
commit: null,
@@ -27,4 +29,7 @@ export default () => ({
projectPath: '',
commentForms: [],
highlightedRow: null,
+ renderTreeList: true,
+ showWhitespace: true,
+ fileFinderVisible: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 0338cde3658..71ad108ce88 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -18,3 +18,8 @@ export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
+
+export const SET_TREE_DATA = 'SET_TREE_DATA';
+export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
+export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
+export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index ed4203cf5e0..5a27388863c 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,5 +1,4 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { sortTree } from '~/ide/stores/utils';
import {
findDiffFile,
addLineReferences,
@@ -7,7 +6,6 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
- generateTreeList,
} from './utils';
import * as types from './mutation_types';
@@ -23,12 +21,9 @@ export default {
[types.SET_DIFF_DATA](state, data) {
prepareDiffData(data);
- const { tree, treeEntries } = generateTreeList(data.diff_files);
Object.assign(state, {
...convertObjectPropsToCamelCase(data),
- tree: sortTree(tree),
- treeEntries,
});
},
@@ -149,6 +144,7 @@ export default {
if (left || right) {
return {
+ ...line,
left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null,
};
@@ -239,4 +235,17 @@ export default {
[types.SET_HIGHLIGHTED_ROW](state, lineCode) {
state.highlightedRow = lineCode;
},
+ [types.SET_TREE_DATA](state, { treeEntries, tree }) {
+ state.treeEntries = treeEntries;
+ state.tree = tree;
+ },
+ [types.SET_RENDER_TREE_LIST](state, renderTreeList) {
+ state.renderTreeList = renderTreeList;
+ },
+ [types.SET_SHOW_WHITESPACE](state, showWhitespace) {
+ state.showWhitespace = showWhitespace;
+ },
+ [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
+ state.fileFinderVisible = visible;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 2fe20551642..247d1e65fea 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
-import { diffModes } from '~/ide/constants';
+import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
+import { diffModes, diffViewerModes } from '~/ide/constants';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -11,6 +12,7 @@ import {
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
+ TREE_TYPE,
} from '../constants';
export function findDiffFile(files, hash) {
@@ -159,6 +161,7 @@ export function addContextLines(options) {
const normalizedParallelLines = contextLines.map(line => ({
left: line,
right: line,
+ line_code: line.line_code,
}));
if (options.bottom) {
@@ -180,8 +183,6 @@ export function addContextLines(options) {
export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
delete line.text;
- // eslint-disable-next-line no-param-reassign
- line.discussions = [];
const parsedLine = Object.assign({}, line);
@@ -221,10 +222,12 @@ export function prepareDiffData(diffData) {
line.line_code = getLineCode(line, u);
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
+ line.left.discussions = [];
line.left.hasForm = false;
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
+ line.right.discussions = [];
line.right.hasForm = false;
}
}
@@ -234,14 +237,19 @@ export function prepareDiffData(diffData) {
const linesLength = file.highlighted_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.highlighted_diff_lines[u];
- Object.assign(line, { ...trimFirstCharOfLineContent(line), hasForm: false });
+ Object.assign(line, {
+ ...trimFirstCharOfLineContent(line),
+ discussions: [],
+ hasForm: false,
+ });
}
showingLines += file.parallel_diff_lines.length;
}
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ collapsed:
+ file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
discussions: [],
});
}
@@ -289,8 +297,63 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
return latestDiff && discussion.active && line_code === discussion.line_code;
}
-export const generateTreeList = files =>
- files.reduce(
+export const getLowestSingleFolder = folder => {
+ const getFolder = (blob, start = []) =>
+ blob.tree.reduce(
+ (acc, file) => {
+ const shouldGetFolder = file.tree.length === 1 && file.tree[0].type === TREE_TYPE;
+ const currentFileTypeTree = file.type === TREE_TYPE;
+ const path = shouldGetFolder || currentFileTypeTree ? acc.path.concat(file.name) : acc.path;
+ const tree = shouldGetFolder || currentFileTypeTree ? acc.tree.concat(file) : acc.tree;
+
+ if (shouldGetFolder) {
+ const firstFolder = getFolder(file);
+
+ path.push(...firstFolder.path);
+ tree.push(...firstFolder.tree);
+ }
+
+ return {
+ ...acc,
+ path,
+ tree,
+ };
+ },
+ { path: start, tree: [] },
+ );
+ const { path, tree } = getFolder(folder, [folder.name]);
+
+ return {
+ path: truncatePathMiddleToLength(path.join('/'), 40),
+ treeAcc: tree.length ? tree[tree.length - 1].tree : null,
+ };
+};
+
+export const flattenTree = tree => {
+ const flatten = blobTree =>
+ blobTree.reduce((acc, file) => {
+ const blob = file;
+ let treeToFlatten = blob.tree;
+
+ if (file.type === TREE_TYPE && file.tree.length === 1) {
+ const { treeAcc, path } = getLowestSingleFolder(file);
+
+ if (treeAcc) {
+ blob.name = path;
+ treeToFlatten = flatten(treeAcc);
+ }
+ }
+
+ blob.tree = flatten(treeToFlatten);
+
+ return acc.concat(blob);
+ }, []);
+
+ return flatten(tree);
+};
+
+export const generateTreeList = files => {
+ const { treeEntries, tree } = files.reduce(
(acc, file) => {
const split = file.new_path.split('/');
@@ -318,6 +381,7 @@ export const generateTreeList = files =>
fileHash: file.file_hash,
addedLines: file.added_lines,
removedLines: file.removed_lines,
+ parentPath: parent ? `${parent.path}/` : '/',
});
} else {
Object.assign(entry, {
@@ -334,11 +398,16 @@ export const generateTreeList = files =>
{ treeEntries: {}, tree: [] },
);
+ return { treeEntries, tree: flattenTree(tree) };
+};
+
export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
return (
diffModes[diffModeKey] ||
- (diffFile.mode_changed && diffModes.mode_changed) ||
+ (diffFile.viewer &&
+ diffFile.viewer.name === diffViewerModes.mode_changed &&
+ diffViewerModes.mode_changed) ||
diffModes.replaced
);
};
diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js
new file mode 100644
index 00000000000..534d737c77e
--- /dev/null
+++ b/app/assets/javascripts/diffs/workers/tree_worker.js
@@ -0,0 +1,14 @@
+import { sortTree } from '~/ide/stores/utils';
+import { generateTreeList } from '../store/utils';
+
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', e => {
+ const { data } = e;
+ const { treeEntries, tree } = generateTreeList(data);
+
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage({
+ treeEntries,
+ tree: sortTree(tree),
+ });
+});
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index d8d0fa1fac4..00e41dd0301 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -25,15 +25,16 @@ class DirtySubmitForm {
DirtySubmitForm.THROTTLE_DURATION,
);
this.form.addEventListener('input', throttledUpdateDirtyInput);
+ this.form.addEventListener('change', throttledUpdateDirtyInput);
this.form.addEventListener('submit', event => this.formSubmit(event));
}
updateDirtyInput(event) {
- const input = event.target;
+ const { target } = event;
- if (!input.dataset.isDirtySubmitInput) return;
+ if (!target.dataset.isDirtySubmitInput) return;
- this.updateDirtyInputs(input);
+ this.updateDirtyInputs(target);
this.toggleSubmission();
}
diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js
deleted file mode 100644
index 5185b019376..00000000000
--- a/app/assets/javascripts/dismissable_callout.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import Flash from '~/flash';
-
-export default function initDismissableCallout(alertSelector) {
- const alertEl = document.querySelector(alertSelector);
- if (!alertEl) {
- return;
- }
-
- const closeButtonEl = alertEl.getElementsByClassName('close')[0];
- const { dismissEndpoint, featureId } = closeButtonEl.dataset;
-
- closeButtonEl.addEventListener('click', () => {
- axios
- .post(dismissEndpoint, {
- feature_name: featureId,
- })
- .then(() => {
- $(alertEl).alert('close');
- })
- .catch(() => {
- Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
- });
- });
-}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index dbfcf8cc921..cb1b1173190 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -64,6 +64,7 @@ class DueDateSelect {
this.saveDueDate(true);
}
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($dueDateInput.val()));
@@ -183,6 +184,7 @@ export default class DueDateSelectors {
onSelect(dateText) {
$datePicker.val(calendar.toString(dateText));
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate(datePickerVal));
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index cd8dff40b88..36542315c4c 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,13 +1,58 @@
import _ from 'underscore';
-import emojiMap from 'emojis/digests.json';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
import emojiAliases from 'emojis/aliases.json';
+import axios from '../lib/utils/axios_utils';
-export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+import AccessorUtilities from '../lib/utils/accessor';
+
+let emojiMap = null;
+let validEmojiNames = null;
+
+export const EMOJI_VERSION = '1';
+const EMOJI_VERSION_LOCALSTORAGE = `EMOJIS_${EMOJI_VERSION}`;
+
+const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+
+export function initEmojiMap() {
+ return new Promise((resolve, reject) => {
+ if (emojiMap) {
+ resolve(emojiMap);
+ } else if (isLocalStorageAvailable && window.localStorage.getItem(EMOJI_VERSION_LOCALSTORAGE)) {
+ emojiMap = JSON.parse(window.localStorage.getItem(EMOJI_VERSION_LOCALSTORAGE));
+ validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ resolve(emojiMap);
+ } else {
+ // We load the JSON from server
+ axios
+ .get(
+ `${gon.asset_host || ''}${gon.relative_url_root ||
+ ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
+ )
+ .then(({ data }) => {
+ emojiMap = data;
+ validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ resolve(emojiMap);
+ if (isLocalStorageAvailable) {
+ window.localStorage.setItem(EMOJI_VERSION_LOCALSTORAGE, JSON.stringify(emojiMap));
+ }
+ })
+ .catch(err => {
+ createFlash(s__('Emojis|Something went wrong while loading emojis.'));
+ reject(err);
+ });
+ }
+ });
+}
export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
+export function getValidEmojiNames() {
+ return validEmojiNames;
+}
+
export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
@@ -36,8 +81,8 @@ export function getEmojiCategoryMap() {
};
Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
- if (emojiCategoryMap[emoji.category]) {
- emojiCategoryMap[emoji.category].push(name);
+ if (emojiCategoryMap[emoji.c]) {
+ emojiCategoryMap[emoji.c].push(name);
}
});
}
@@ -58,8 +103,9 @@ export function getEmojiInfo(query) {
}
export function emojiFallbackImageSrc(inputName) {
- const { name, digest } = getEmojiInfo(inputName);
- return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`;
+ const { name } = getEmojiInfo(inputName);
+ return `${gon.asset_host || ''}${gon.relative_url_root ||
+ ''}/-/emojis/${EMOJI_VERSION}/${name}.png`;
}
export function emojiImageTag(name, src) {
@@ -68,9 +114,8 @@ export function emojiImageTag(name, src) {
export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options };
- const { name, ...emojiInfo } = getEmojiInfo(inputName);
+ const name = normalizeEmojiName(inputName);
- const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
@@ -79,24 +124,19 @@ export function glEmojiTag(inputName, options) {
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);
- }
+ const forceFallbackAttribute = opts.forceFallback ? 'data-force-fallback="true"' : '';
return `
<gl-emoji
${classAttribute}
data-name="${name}"
- data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
- data-unicode-version="${emojiInfo.unicodeVersion}"
- title="${emojiInfo.description}"
+ ${forceFallbackAttribute}
>
- ${contents}
</gl-emoji>
`;
}
diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js
new file mode 100644
index 00000000000..0fd4dd74953
--- /dev/null
+++ b/app/assets/javascripts/emoji/no_emoji_validator.js
@@ -0,0 +1,63 @@
+import { __ } from '~/locale';
+import emojiRegex from 'emoji-regex';
+
+const invalidInputClass = 'gl-field-error-outline';
+
+export default class NoEmojiValidator {
+ constructor(opts = {}) {
+ const container = opts.container || '';
+ this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
+
+ this.noEmojiEmelents.forEach(element =>
+ element.addEventListener('input', this.eventHandler.bind(this)),
+ );
+ }
+
+ eventHandler(event) {
+ this.inputDomElement = event.target;
+ this.inputErrorMessage = this.inputDomElement.nextSibling;
+
+ const { value } = this.inputDomElement;
+
+ this.validatePattern(value);
+ this.setValidationStateAndMessage();
+ }
+
+ validatePattern(value) {
+ const pattern = emojiRegex();
+ this.hasEmojis = new RegExp(pattern).test(value);
+
+ if (this.hasEmojis) {
+ this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
+ } else {
+ this.inputDomElement.setCustomValidity('');
+ }
+ }
+
+ setValidationStateAndMessage() {
+ if (!this.inputDomElement.checkValidity()) {
+ this.setInvalidState();
+ } else {
+ this.clearFieldValidationState();
+ }
+ }
+
+ clearFieldValidationState() {
+ this.inputDomElement.classList.remove(invalidInputClass);
+ this.inputErrorMessage.classList.add('hide');
+ }
+
+ setInvalidState() {
+ this.inputDomElement.classList.add(invalidInputClass);
+ this.setErrorMessage();
+ }
+
+ setErrorMessage() {
+ if (this.hasEmojis) {
+ this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
+ } else {
+ this.inputErrorMessage.innerHTML = this.inputDomElement.title;
+ }
+ this.inputErrorMessage.classList.remove('hide');
+ }
+}
diff --git a/app/assets/javascripts/emoji/support/index.js b/app/assets/javascripts/emoji/support/index.js
index 1f7852dd487..ed9bfb8bc78 100644
--- a/app/assets/javascripts/emoji/support/index.js
+++ b/app/assets/javascripts/emoji/support/index.js
@@ -5,6 +5,9 @@ import getUnicodeSupportMap from './unicode_support_map';
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
+ // Our Spec browser would fail producing emoji maps
+ if (/\bHeadlessChrome\//.test(navigator.userAgent)) return true;
+
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
new file mode 100644
index 00000000000..a8ee3f4ac10
--- /dev/null
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -0,0 +1,108 @@
+<script>
+/**
+ * Render modal to confirm rollback/redeploy.
+ */
+
+import _ from 'underscore';
+import { GlModal } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+import eventHub from '../event_hub';
+
+export default {
+ name: 'ConfirmRollbackModal',
+
+ components: {
+ GlModal,
+ },
+
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ modalTitle() {
+ const title = this.environment.isLastDeployment
+ ? s__('Environments|Re-deploy environment %{name}?')
+ : s__('Environments|Rollback environment %{name}?');
+
+ return sprintf(title, {
+ name: _.escape(this.environment.name),
+ });
+ },
+
+ commitShortSha() {
+ const { last_deployment } = this.environment;
+ return this.commitData(last_deployment, 'short_id');
+ },
+
+ commitUrl() {
+ const { last_deployment } = this.environment;
+ return this.commitData(last_deployment, 'commit_path');
+ },
+
+ commitTitle() {
+ const { last_deployment } = this.environment;
+ return this.commitData(last_deployment, 'title');
+ },
+
+ modalText() {
+ const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`;
+ const commitId = _.escape(this.commitShortSha);
+ const linkEnd = '</a>';
+ const name = _.escape(this.name);
+ const body = this.environment.isLastDeployment
+ ? s__(
+ 'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
+ )
+ : s__(
+ 'Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?',
+ );
+ return sprintf(
+ body,
+ {
+ commitId,
+ linkStart,
+ linkEnd,
+ name,
+ },
+ false,
+ );
+ },
+
+ modalActionText() {
+ return this.environment.isLastDeployment
+ ? s__('Environments|Re-deploy')
+ : s__('Environments|Rollback');
+ },
+ },
+
+ methods: {
+ onOk() {
+ eventHub.$emit('rollbackEnvironment', this.environment);
+ },
+
+ commitData(lastDeployment, key) {
+ if (lastDeployment && lastDeployment.commit) {
+ return lastDeployment.commit[key];
+ }
+
+ return '';
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ :title="modalTitle"
+ modal-id="confirm-rollback-modal"
+ :ok-title="modalActionText"
+ ok-variant="danger"
+ @ok="onOk"
+ >
+ <p v-html="modalText"></p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index bd402c0eea5..6ece8b92a30 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -51,11 +47,7 @@ export default {
<slot name="emptyState"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table
- :environments="environments"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 1f7dab9fbd2..208bd19f6b0 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -92,7 +92,7 @@ export default {
:disabled="isActionDisabled(action)"
type="button"
class="js-manual-action-link no-btn btn d-flex align-items-center"
- @click="onClickAction(action);"
+ @click="onClickAction(action)"
>
<span class="flex-fill"> {{ action.name }} </span>
<span v-if="action.scheduledAt" class="text-secondary">
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index f44806d82a6..1e89dce69cb 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -3,7 +3,6 @@ import Timeago from 'timeago.js';
import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
@@ -47,12 +46,6 @@ export default {
default: () => ({}),
},
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
canReadEnvironment: {
type: Boolean,
required: false,
@@ -151,7 +144,7 @@ export default {
},
actions() {
- if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
+ if (!this.model || !this.model.last_deployment) {
return [];
}
@@ -162,7 +155,7 @@ export default {
const combinedActions = (manualActions || []).concat(scheduledActions || []);
return combinedActions.map(action => ({
...action,
- name: humanize(action.name),
+ name: action.name,
}));
},
@@ -561,7 +554,8 @@ export default {
/>
<rollback-component
- v-if="canRetry && canCreateDeployment"
+ v-if="canRetry"
+ :environment="model"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
/>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 50c86af057c..266cdc42518 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -5,29 +5,38 @@
*
* Makes a post request when the button is clicked.
*/
+import { GlTooltipDirective, GlLoadingIcon, GlModalDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
-import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import eventHub from '../event_hub';
export default {
components: {
Icon,
GlLoadingIcon,
+ GlButton,
+ ConfirmRollbackModal,
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
},
props: {
- retryUrl: {
- type: String,
- default: '',
- },
-
isLastDeployment: {
type: Boolean,
default: true,
},
+
+ environment: {
+ type: Object,
+ required: true,
+ },
+
+ retryUrl: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -45,23 +54,31 @@ export default {
methods: {
onClick() {
- this.isLoading = true;
-
- eventHub.$emit('postAction', { endpoint: this.retryUrl });
+ eventHub.$emit('requestRollbackEnvironment', {
+ ...this.environment,
+ retryUrl: this.retryUrl,
+ isLastDeployment: this.isLastDeployment,
+ });
+ eventHub.$on('rollbackEnvironment', environment => {
+ if (environment.id === this.environment.id) {
+ this.isLoading = true;
+ }
+ });
},
},
};
</script>
<template>
- <button
+ <gl-button
v-gl-tooltip
+ v-gl-modal.confirm-rollback-modal
+ variant="secondary"
:disabled="isLoading"
:title="title"
- type="button"
- class="btn d-none d-sm-none d-md-block"
+ class="d-none d-md-block"
@click="onClick"
>
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
<gl-loading-icon v-if="isLoading" />
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index ae9459a2482..6e55c3f901a 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -6,11 +6,13 @@ import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
components: {
emptyState,
StopEnvironmentModal,
+ ConfirmRollbackModal,
},
mixins: [CIPaginationMixin, environmentsMixin],
@@ -24,10 +26,6 @@ export default {
type: Boolean,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -91,14 +89,15 @@ export default {
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
+ <confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
<div v-if="canCreateEnvironment && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-success">
- {{ s__('Environments|New environment') }}
- </a>
+ <a :href="newEnvironmentPath" class="btn btn-success">{{
+ s__('Environments|New environment')
+ }}</a>
</div>
</div>
@@ -106,7 +105,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 533e90e2222..eef141a07ba 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -3,6 +3,7 @@
* Render environments table.
*/
import { GlLoadingIcon } from '@gitlab/ui';
+import _ from 'underscore';
import environmentItem from './environment_item.vue';
export default {
@@ -23,11 +24,14 @@ export default {
required: false,
default: false,
},
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
+ },
+ computed: {
+ sortedEnvironments() {
+ return this.sortEnvironments(this.environments).map(env =>
+ this.shouldRenderFolderContent(env)
+ ? { ...env, children: this.sortEnvironments(env.children) }
+ : env,
+ );
},
},
methods: {
@@ -37,6 +41,30 @@ export default {
shouldRenderFolderContent(env) {
return env.isFolder && env.isOpen && env.children && env.children.length > 0;
},
+ sortEnvironments(environments) {
+ /*
+ * The sorting algorithm should sort in the following priorities:
+ *
+ * 1. folders first,
+ * 2. last updated descending,
+ * 3. by name ascending,
+ *
+ * the sorting algorithm must:
+ *
+ * 1. Sort by name ascending,
+ * 2. Reverse (sort by name descending),
+ * 3. Sort by last deployment ascending,
+ * 4. Reverse (last deployment descending, name ascending),
+ * 5. Put folders first.
+ */
+ return _.chain(environments)
+ .sortBy(env => (env.isFolder ? env.folderName : env.name))
+ .reverse()
+ .sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000'))
+ .reverse()
+ .sortBy(env => (env.isFolder ? -1 : 1))
+ .value();
+ },
},
};
</script>
@@ -59,12 +87,11 @@ export default {
{{ s__('Environments|Updated') }}
</div>
</div>
- <template v-for="(model, i) in environments" :model="model">
+ <template v-for="(model, i) in sortedEnvironments" :model="model">
<div
is="environment-item"
:key="`environment-item-${i}`"
:model="model"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
@@ -79,15 +106,14 @@ export default {
v-for="(children, index) in model.children"
:key="`env-item-${i}-${index}`"
:model="children"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<div :key="`sub-div-${i}`">
<div class="text-center prepend-top-10">
- <a :href="folderUrl(model)" class="btn btn-default">
- {{ s__('Environments|Show all') }}
- </a>
+ <a :href="folderUrl(model)" class="btn btn-default">{{
+ s__('Environments|Show all')
+ }}</a>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 3cf6e4ad14d..56e7f69cad6 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -15,11 +15,10 @@ export default () =>
const environmentsData = document.querySelector(this.$options.el).dataset;
return {
- endpoint: environmentsData.endpoint,
- folderName: environmentsData.folderName,
+ endpoint: environmentsData.environmentsDataEndpoint,
+ folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass,
- canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment),
- canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
+ canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
};
},
render(createElement) {
@@ -28,7 +27,6 @@ export default () =>
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
- canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index d6f0b6115a6..80f0e00400b 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -23,10 +23,6 @@ export default {
type: String,
required: true,
},
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
canReadEnvironment: {
type: Boolean,
required: true,
@@ -55,7 +51,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index d366e7550b7..6af66d0f86e 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -20,7 +20,6 @@ export default () =>
helpPagePath: environmentsData.helpPagePath,
cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
},
@@ -32,7 +31,6 @@ export default () =>
helpPagePath: this.helpPagePath,
cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
- canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 96dc1f07cb9..71b6b578196 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -36,6 +36,7 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
+ environmentInRollbackModal: {},
};
},
@@ -43,7 +44,11 @@ export default {
saveData(resp) {
this.isLoading = false;
- if (_.isEqual(resp.config.params, this.requestData)) {
+ // Prevent the absence of the nested flag from causing mismatches
+ const response = this.filterNilValues(resp.config.params);
+ const request = this.filterNilValues(this.requestData);
+
+ if (_.isEqual(response, request)) {
this.store.storeAvailableCount(resp.data.available_count);
this.store.storeStoppedCount(resp.data.stopped_count);
this.store.storeEnvironments(resp.data.environments);
@@ -51,6 +56,10 @@ export default {
}
},
+ filterNilValues(obj) {
+ return _.omit(obj, value => _.isUndefined(value) || _.isNull(value));
+ },
+
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
@@ -64,10 +73,9 @@ export default {
// fetch new data
return this.service
.fetchEnvironments(this.requestData)
- .then(response => this.successCallback(response))
- .then(() => {
- // restart polling
- this.poll.restart({ data: this.requestData });
+ .then(response => {
+ this.successCallback(response);
+ this.poll.enable({ data: this.requestData, response });
})
.catch(() => {
this.errorCallback();
@@ -109,6 +117,10 @@ export default {
this.environmentInStopModal = environment;
},
+ updateRollbackModal(environment) {
+ this.environmentInRollbackModal = environment;
+ },
+
stopEnvironment(environment) {
const endpoint = environment.stop_path;
const errorMessage = s__(
@@ -116,6 +128,16 @@ export default {
);
this.postAction({ endpoint, errorMessage });
},
+
+ rollbackEnvironment(environment) {
+ const { retryUrl, isLastDeployment } = environment;
+ const errorMessage = isLastDeployment
+ ? s__('Environments|An error occurred while re-deploying the environment, please try again')
+ : s__(
+ 'Environments|An error occurred while rolling back the environment, please try again',
+ );
+ this.postAction({ endpoint: retryUrl, errorMessage });
+ },
},
computed: {
@@ -143,7 +165,7 @@ export default {
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
@@ -174,11 +196,17 @@ export default {
eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
+
+ eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
+ eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
+
+ eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
+ eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
},
};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 4e07ccba91a..cb4ff6856db 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -7,8 +7,8 @@ export default class EnvironmentsService {
}
fetchEnvironments(options = {}) {
- const { scope, page } = options;
- return axios.get(this.environmentsEndpoint, { params: { scope, page } });
+ const { scope, page, nested } = options;
+ return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5808a2d4afa..ac9a31c202c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -20,7 +20,8 @@ export default class EnvironmentsStore {
*
* Stores the received environments.
*
- * In the main environments endpoint, each environment has the following schema
+ * In the main environments endpoint (with { nested: true } in params), each folder
+ * 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.
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
new file mode 100644
index 00000000000..43ae54133af
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -0,0 +1,124 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { __ } from '~/locale';
+
+export default {
+ fields: [
+ { key: 'error', label: __('Open errors') },
+ { key: 'events', label: __('Events') },
+ { key: 'users', label: __('Users') },
+ { key: 'lastSeen', label: __('Last seen') },
+ ],
+ components: {
+ GlEmptyState,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTable,
+ Icon,
+ TimeAgo,
+ },
+ props: {
+ indexPath: {
+ type: String,
+ required: true,
+ },
+ enableErrorTrackingLink: {
+ type: String,
+ required: true,
+ },
+ errorTrackingEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ illustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['errors', 'externalUrl', 'loading']),
+ },
+ created() {
+ if (this.errorTrackingEnabled) {
+ this.startPolling(this.indexPath);
+ }
+ },
+ methods: {
+ ...mapActions(['startPolling', 'restartPolling']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="errorTrackingEnabled">
+ <div v-if="loading" class="py-3">
+ <gl-loading-icon :size="3" />
+ </div>
+ <div v-else>
+ <div class="d-flex justify-content-end">
+ <gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank">
+ {{ __('View in Sentry') }}
+ <icon name="external-link" />
+ </gl-button>
+ </div>
+ <gl-table :items="errors" :fields="$options.fields" :show-empty="true">
+ <template slot="HEAD_events" slot-scope="data">
+ <div class="text-right">{{ data.label }}</div>
+ </template>
+ <template slot="HEAD_users" slot-scope="data">
+ <div class="text-right">{{ data.label }}</div>
+ </template>
+ <template slot="error" slot-scope="errors">
+ <div class="d-flex flex-column">
+ <div class="d-flex">
+ <gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank">
+ <strong>{{ errors.item.title.trim() }}</strong>
+ <icon name="external-link" class="ml-1" />
+ </gl-link>
+ <span class="text-secondary ml-2">{{ errors.item.culprit }}</span>
+ </div>
+ {{ errors.item.message || __('No details available') }}
+ </div>
+ </template>
+
+ <template slot="events" slot-scope="errors">
+ <div class="text-right">{{ errors.item.count }}</div>
+ </template>
+
+ <template slot="users" slot-scope="errors">
+ <div class="text-right">{{ errors.item.userCount }}</div>
+ </template>
+
+ <template slot="lastSeen" slot-scope="errors">
+ <div class="d-flex align-items-center">
+ <icon name="calendar" css-classes="text-secondary mr-1" />
+ <time-ago :time="errors.item.lastSeen" class="text-secondary" />
+ </div>
+ </template>
+ <template slot="empty">
+ <div ref="empty">
+ {{ __('No errors to display.') }}
+ <gl-link class="js-try-again" @click="restartPolling">
+ {{ __('Check again') }}
+ </gl-link>
+ </div>
+ </template>
+ </gl-table>
+ </div>
+ </div>
+ <div v-else>
+ <gl-empty-state
+ :title="__('Get started with error tracking')"
+ :description="__('Monitor your errors by integrating with Sentry')"
+ :primary-button-text="__('Enable error tracking')"
+ :primary-button-link="enableErrorTrackingLink"
+ :svg-path="illustrationPath"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js
new file mode 100644
index 00000000000..3d609448efe
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import store from './store';
+import ErrorTrackingList from './components/error_tracking_list.vue';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-error_tracking',
+ components: {
+ ErrorTrackingList,
+ },
+ store,
+ render(createElement) {
+ const domEl = document.querySelector(this.$options.el);
+ const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset;
+ let { errorTrackingEnabled } = domEl.dataset;
+
+ errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
+
+ return createElement('error-tracking-list', {
+ props: {
+ indexPath,
+ enableErrorTrackingLink,
+ errorTrackingEnabled,
+ illustrationPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js
new file mode 100644
index 00000000000..ab89521dc46
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/services/index.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ getErrorList({ endpoint }) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
new file mode 100644
index 00000000000..d42e4f145dc
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -0,0 +1,52 @@
+import Service from '../services';
+import * as types from './mutation_types';
+import createFlash from '~/flash';
+import Poll from '~/lib/utils/poll';
+import { __, sprintf } from '~/locale';
+
+let eTagPoll;
+
+export function startPolling({ commit, dispatch }, endpoint) {
+ eTagPoll = new Poll({
+ resource: Service,
+ method: 'getErrorList',
+ data: { endpoint },
+ successCallback: ({ data }) => {
+ if (!data) {
+ return;
+ }
+ commit(types.SET_ERRORS, data.errors);
+ commit(types.SET_EXTERNAL_URL, data.external_url);
+ commit(types.SET_LOADING, false);
+ dispatch('stopPolling');
+ },
+ errorCallback: response => {
+ let errorMessage = '';
+ if (response && response.data && response.data.message) {
+ errorMessage = response.data.message;
+ }
+ commit(types.SET_LOADING, false);
+ createFlash(
+ sprintf(__(`Failed to load errors from Sentry. Error message: %{errorMessage}`), {
+ errorMessage,
+ }),
+ );
+ },
+ });
+
+ eTagPoll.makeRequest();
+}
+
+export const stopPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+
+export function restartPolling({ commit }) {
+ commit(types.SET_ERRORS, []);
+ commit(types.SET_EXTERNAL_URL, '');
+ commit(types.SET_LOADING, true);
+
+ if (eTagPoll) eTagPoll.restart();
+}
+
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js
new file mode 100644
index 00000000000..3136682fb64
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ state: {
+ errors: [],
+ externalUrl: '',
+ loading: true,
+ },
+ actions,
+ mutations,
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/mutation_types.js
new file mode 100644
index 00000000000..f9d77a6b08e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_ERRORS = 'SET_ERRORS';
+export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
+export const SET_LOADING = 'SET_LOADING';
diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/mutations.js
new file mode 100644
index 00000000000..e4bd81db9c9
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/mutations.js
@@ -0,0 +1,14 @@
+import * as types from './mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default {
+ [types.SET_ERRORS](state, data) {
+ state.errors = convertObjectPropsToCamelCase(data, { deep: true });
+ },
+ [types.SET_EXTERNAL_URL](state, url) {
+ state.externalUrl = url;
+ },
+ [types.SET_LOADING](state, loading) {
+ state.loading = loading;
+ },
+};
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
new file mode 100644
index 00000000000..50eb3e63b7c
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import ProjectDropdown from './project_dropdown.vue';
+import ErrorTrackingForm from './error_tracking_form.vue';
+
+export default {
+ components: { ProjectDropdown, ErrorTrackingForm, GlButton },
+ props: {
+ initialApiHost: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialEnabled: {
+ type: String,
+ required: true,
+ },
+ initialProject: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ initialToken: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ listProjectsEndpoint: {
+ type: String,
+ required: true,
+ },
+ operationsSettingsEndpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters([
+ 'dropdownLabel',
+ 'hasProjects',
+ 'invalidProjectLabel',
+ 'isProjectInvalid',
+ 'projectSelectionLabel',
+ ]),
+ ...mapState([
+ 'apiHost',
+ 'connectError',
+ 'connectSuccessful',
+ 'enabled',
+ 'projects',
+ 'selectedProject',
+ 'settingsLoading',
+ 'token',
+ ]),
+ },
+ created() {
+ this.setInitialState({
+ apiHost: this.initialApiHost,
+ enabled: this.initialEnabled,
+ project: this.initialProject,
+ token: this.initialToken,
+ listProjectsEndpoint: this.listProjectsEndpoint,
+ operationsSettingsEndpoint: this.operationsSettingsEndpoint,
+ });
+ },
+ methods: {
+ ...mapActions([
+ 'fetchProjects',
+ 'setInitialState',
+ 'updateApiHost',
+ 'updateEnabled',
+ 'updateSelectedProject',
+ 'updateSettings',
+ 'updateToken',
+ ]),
+ handleSubmit() {
+ this.updateSettings();
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="form-check form-group">
+ <input
+ id="error-tracking-enabled"
+ :checked="enabled"
+ class="form-check-input"
+ type="checkbox"
+ @change="updateEnabled($event.target.checked)"
+ />
+ <label class="form-check-label" for="error-tracking-enabled">{{
+ s__('ErrorTracking|Active')
+ }}</label>
+ </div>
+ <error-tracking-form
+ :api-host="apiHost"
+ :connect-error="connectError"
+ :connect-successful="connectSuccessful"
+ :token="token"
+ @handle-connect="fetchProjects"
+ @update-api-host="updateApiHost"
+ @update-token="updateToken"
+ />
+ <div class="form-group">
+ <project-dropdown
+ :has-projects="hasProjects"
+ :invalid-project-label="invalidProjectLabel"
+ :is-project-invalid="isProjectInvalid"
+ :dropdown-label="dropdownLabel"
+ :project-selection-label="projectSelectionLabel"
+ :projects="projects"
+ :selected-project="selectedProject"
+ :token="token"
+ @select-project="updateSelectedProject"
+ />
+ </div>
+ <gl-button
+ :disabled="settingsLoading"
+ class="js-error-tracking-button"
+ variant="success"
+ @click="handleSubmit"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
new file mode 100644
index 00000000000..060d8e25227
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlButton, GlFormInput } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: { GlButton, GlFormInput, Icon },
+ props: {
+ apiHost: {
+ type: String,
+ required: true,
+ },
+ connectError: {
+ type: Boolean,
+ required: true,
+ },
+ connectSuccessful: {
+ type: Boolean,
+ required: true,
+ },
+ token: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ tokenInputState() {
+ return this.connectError ? false : null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="form-group">
+ <label class="label-bold" for="error-tracking-api-host">{{ __('Sentry API URL') }}</label>
+ <div class="row">
+ <div class="col-8 col-md-9 gl-pr-0">
+ <gl-form-input
+ id="error-tracking-api-host"
+ :value="apiHost"
+ placeholder="https://mysentryserver.com"
+ @input="$emit('update-api-host', $event)"
+ />
+ </div>
+ </div>
+ <p class="form-text text-muted">
+ {{ s__('ErrorTracking|Find your hostname in your Sentry account settings page') }}
+ </p>
+ </div>
+ <div class="form-group" :class="{ 'gl-show-field-errors': connectError }">
+ <label class="label-bold" for="error-tracking-token">{{
+ s__('ErrorTracking|Auth Token')
+ }}</label>
+ <div class="row">
+ <div class="col-8 col-md-9 gl-pr-0">
+ <gl-form-input
+ id="error-tracking-token"
+ :value="token"
+ :state="tokenInputState"
+ @input="$emit('update-token', $event)"
+ />
+ </div>
+ <div class="col-4 col-md-3 gl-pl-0">
+ <gl-button
+ class="js-error-tracking-connect prepend-left-5"
+ @click="$emit('handle-connect')"
+ >
+ {{ __('Connect') }}
+ </gl-button>
+ <icon
+ v-show="connectSuccessful"
+ class="js-error-tracking-connect-success prepend-left-5 text-success align-middle"
+ :aria-label="__('Projects Successfully Retrieved')"
+ name="check-circle"
+ />
+ </div>
+ </div>
+ <p v-if="connectError" class="gl-field-error">
+ {{ s__('ErrorTracking|Connection has failed. Re-check Auth Token and try again.') }}
+ </p>
+ <p v-else class="form-text text-muted">
+ {{
+ s__(
+ "ErrorTracking|After adding your Auth Token, use the 'Connect' button to load projects",
+ )
+ }}
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
new file mode 100644
index 00000000000..82df02afafd
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { getDisplayName } from '../utils';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownHeader,
+ GlDropdownItem,
+ Icon,
+ },
+ props: {
+ dropdownLabel: {
+ type: String,
+ required: true,
+ },
+ hasProjects: {
+ type: Boolean,
+ required: true,
+ },
+ invalidProjectLabel: {
+ type: String,
+ required: true,
+ },
+ isProjectInvalid: {
+ type: Boolean,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
+ selectedProject: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ projectSelectionLabel: {
+ type: String,
+ required: true,
+ },
+ token: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ getDisplayName,
+ },
+};
+</script>
+
+<template>
+ <div :class="{ 'gl-show-field-errors': isProjectInvalid }">
+ <label class="label-bold" for="project-dropdown">{{ __('Project') }}</label>
+ <div class="row">
+ <gl-dropdown
+ id="project-dropdown"
+ class="col-8 col-md-9 gl-pr-0"
+ :disabled="!hasProjects"
+ menu-class="w-100 mw-100"
+ toggle-class="dropdown-menu-toggle w-100 gl-field-error-outline"
+ :text="dropdownLabel"
+ >
+ <gl-dropdown-item
+ v-for="project in projects"
+ :key="`${project.organizationSlug}.${project.slug}`"
+ class="w-100"
+ @click="$emit('select-project', project)"
+ >{{ getDisplayName(project) }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
+ <p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error">
+ {{ invalidProjectLabel }}
+ </p>
+ <p v-else-if="!hasProjects" class="js-project-dropdown-label form-text text-muted">
+ {{ projectSelectionLabel }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
new file mode 100644
index 00000000000..ce315963723
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import ErrorTrackingSettings from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+ const formContainerEl = document.querySelector('.js-error-tracking-form');
+ const {
+ dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
+ } = formContainerEl;
+
+ return new Vue({
+ el: formContainerEl,
+ store: createStore(),
+ render(createElement) {
+ return createElement(ErrorTrackingSettings, {
+ props: {
+ initialApiHost: apiHost,
+ initialEnabled: enabled,
+ initialProject: project,
+ initialToken: token,
+ listProjectsEndpoint,
+ operationsSettingsEndpoint,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
new file mode 100644
index 00000000000..95105797807
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -0,0 +1,91 @@
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import { transformFrontendSettings } from '../utils';
+import * as types from './mutation_types';
+
+export const requestProjects = ({ commit }) => {
+ commit(types.RESET_CONNECT);
+};
+
+export const receiveProjectsSuccess = ({ commit }, projects) => {
+ commit(types.UPDATE_CONNECT_SUCCESS);
+ commit(types.RECEIVE_PROJECTS, projects);
+};
+
+export const receiveProjectsError = ({ commit }) => {
+ commit(types.UPDATE_CONNECT_ERROR);
+ commit(types.CLEAR_PROJECTS);
+};
+
+export const fetchProjects = ({ dispatch, state }) => {
+ dispatch('requestProjects');
+ return axios
+ .post(state.listProjectsEndpoint, {
+ error_tracking_setting: {
+ api_host: state.apiHost,
+ token: state.token,
+ },
+ })
+ .then(({ data: { projects } }) => {
+ dispatch('receiveProjectsSuccess', projects);
+ })
+ .catch(() => {
+ dispatch('receiveProjectsError');
+ });
+};
+
+export const requestSettings = ({ commit }) => {
+ commit(types.UPDATE_SETTINGS_LOADING, true);
+};
+
+export const receiveSettingsError = ({ commit }, { response = {} }) => {
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+ commit(types.UPDATE_SETTINGS_LOADING, false);
+};
+
+export const updateSettings = ({ dispatch, state }) => {
+ dispatch('requestSettings');
+ return axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ error_tracking_setting_attributes: {
+ ...transformFrontendSettings(state),
+ },
+ },
+ })
+ .then(() => {
+ refreshCurrentPage();
+ })
+ .catch(err => {
+ dispatch('receiveSettingsError', err);
+ });
+};
+
+export const updateApiHost = ({ commit }, apiHost) => {
+ commit(types.UPDATE_API_HOST, apiHost);
+ commit(types.RESET_CONNECT);
+};
+
+export const updateEnabled = ({ commit }, enabled) => {
+ commit(types.UPDATE_ENABLED, enabled);
+};
+
+export const updateToken = ({ commit }, token) => {
+ commit(types.UPDATE_TOKEN, token);
+ commit(types.RESET_CONNECT);
+};
+
+export const updateSelectedProject = ({ commit }, selectedProject) => {
+ commit(types.UPDATE_SELECTED_PROJECT, selectedProject);
+};
+
+export const setInitialState = ({ commit }, data) => {
+ commit(types.SET_INITIAL_STATE, data);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
new file mode 100644
index 00000000000..a008b181907
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -0,0 +1,44 @@
+import _ from 'underscore';
+import { __, s__, sprintf } from '~/locale';
+import { getDisplayName } from '../utils';
+
+export const hasProjects = state => !!state.projects && state.projects.length > 0;
+
+export const isProjectInvalid = (state, getters) =>
+ !!state.selectedProject &&
+ getters.hasProjects &&
+ !state.projects.some(project => _.isMatch(state.selectedProject, project));
+
+export const dropdownLabel = (state, getters) => {
+ if (state.selectedProject !== null) {
+ return getDisplayName(state.selectedProject);
+ }
+ if (!getters.hasProjects) {
+ return s__('ErrorTracking|No projects available');
+ }
+ return s__('ErrorTracking|Select project');
+};
+
+export const invalidProjectLabel = state => {
+ if (state.selectedProject) {
+ return sprintf(
+ __('Project "%{name}" is no longer available. Select another project to continue.'),
+ {
+ name: state.selectedProject.name,
+ },
+ );
+ }
+ return '';
+};
+
+export const projectSelectionLabel = state => {
+ if (state.token) {
+ return s__(
+ "ErrorTracking|Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.",
+ );
+ }
+ return s__('ErrorTracking|To enable project selection, enter a valid Auth Token');
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking_settings/store/index.js b/app/assets/javascripts/error_tracking_settings/store/index.js
new file mode 100644
index 00000000000..560f265a2ea
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ state: createState(),
+ actions,
+ getters,
+ mutations,
+ });
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
new file mode 100644
index 00000000000..b4f8a237947
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
@@ -0,0 +1,11 @@
+export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+export const RECEIVE_PROJECTS = 'RECEIVE_PROJECTS';
+export const RESET_CONNECT = 'RESET_CONNECT';
+export const UPDATE_API_HOST = 'UPDATE_API_HOST';
+export const UPDATE_CONNECT_ERROR = 'UPDATE_CONNECT_ERROR';
+export const UPDATE_CONNECT_SUCCESS = 'UPDATE_CONNECT_SUCCESS';
+export const UPDATE_ENABLED = 'UPDATE_ENABLED';
+export const UPDATE_SELECTED_PROJECT = 'UPDATE_SELECTED_PROJECT';
+export const UPDATE_SETTINGS_LOADING = 'UPDATE_SETTINGS_LOADING';
+export const UPDATE_TOKEN = 'UPDATE_TOKEN';
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
new file mode 100644
index 00000000000..4089d1ee94e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -0,0 +1,61 @@
+import _ from 'underscore';
+import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+import { projectKeys } from '../utils';
+
+export default {
+ [types.CLEAR_PROJECTS](state) {
+ state.projects = [];
+ },
+ [types.RECEIVE_PROJECTS](state, projects) {
+ state.projects = projects
+ .map(convertObjectPropsToCamelCase)
+ // The `pick` strips out extra properties returned from Sentry.
+ // Such properties could be problematic later, e.g. when checking whether `projects` contains `selectedProject`
+ .map(project => _.pick(project, projectKeys));
+ },
+ [types.RESET_CONNECT](state) {
+ state.connectSuccessful = false;
+ state.connectError = false;
+ },
+ [types.SET_INITIAL_STATE](
+ state,
+ { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
+ ) {
+ state.enabled = parseBoolean(enabled);
+ state.apiHost = apiHost;
+ state.token = token;
+ state.listProjectsEndpoint = listProjectsEndpoint;
+ state.operationsSettingsEndpoint = operationsSettingsEndpoint;
+
+ if (project) {
+ state.selectedProject = _.pick(
+ convertObjectPropsToCamelCase(JSON.parse(project)),
+ projectKeys,
+ );
+ }
+ },
+ [types.UPDATE_API_HOST](state, apiHost) {
+ state.apiHost = apiHost;
+ },
+ [types.UPDATE_ENABLED](state, enabled) {
+ state.enabled = enabled;
+ },
+ [types.UPDATE_TOKEN](state, token) {
+ state.token = token;
+ },
+ [types.UPDATE_SELECTED_PROJECT](state, selectedProject) {
+ state.selectedProject = selectedProject;
+ },
+ [types.UPDATE_SETTINGS_LOADING](state, settingsLoading) {
+ state.settingsLoading = settingsLoading;
+ },
+ [types.UPDATE_CONNECT_SUCCESS](state) {
+ state.connectSuccessful = true;
+ state.connectError = false;
+ },
+ [types.UPDATE_CONNECT_ERROR](state) {
+ state.connectSuccessful = false;
+ state.connectError = true;
+ },
+};
diff --git a/app/assets/javascripts/error_tracking_settings/store/state.js b/app/assets/javascripts/error_tracking_settings/store/state.js
new file mode 100644
index 00000000000..98219d33f4d
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/store/state.js
@@ -0,0 +1,12 @@
+export default () => ({
+ apiHost: '',
+ enabled: false,
+ token: '',
+ projects: [],
+ selectedProject: null,
+ settingsLoading: false,
+ connectSuccessful: false,
+ connectError: false,
+ listProjectsEndpoint: '',
+ operationsSettingsEndpoint: '',
+});
diff --git a/app/assets/javascripts/error_tracking_settings/utils.js b/app/assets/javascripts/error_tracking_settings/utils.js
new file mode 100644
index 00000000000..6613e04ee0e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/utils.js
@@ -0,0 +1,18 @@
+export const projectKeys = ['name', 'organizationName', 'organizationSlug', 'slug'];
+
+export const transformFrontendSettings = ({ apiHost, enabled, token, selectedProject }) => {
+ const project = selectedProject
+ ? {
+ slug: selectedProject.slug,
+ name: selectedProject.name,
+ organization_name: selectedProject.organizationName,
+ organization_slug: selectedProject.organizationSlug,
+ }
+ : null;
+
+ return { api_host: apiHost || null, enabled, token: token || null, project };
+};
+
+export const getDisplayName = project => `${project.organizationName} | ${project.name}`;
+
+export default () => {};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 173fe7c69de..be55e6923c6 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -31,12 +31,14 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
.removeAttr('disabled');
}
+const getPriority = e => parseInt(e.dataset.highlightPriority, 10) || 0;
+
export function findHighestPriorityFeature() {
let priorityFeature;
const sortedFeatureEls = [].slice
.call(document.querySelectorAll('.js-feature-highlight'))
- .sort((a, b) => (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
+ .sort((a, b) => getPriority(b) - getPriority(a));
const [priorityFeatureEl] = sortedFeatureEls;
if (priorityFeatureEl) {
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
new file mode 100644
index 00000000000..54ea936252e
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -0,0 +1,16 @@
+export default IssuableTokenKeys => {
+ const wipToken = {
+ key: 'wip',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'admin',
+ tag: 'Yes or No',
+ lowercaseValueOnSubmit: true,
+ uppercaseTokenName: true,
+ capitalizeTokenValue: true,
+ };
+
+ IssuableTokenKeys.tokenKeys.push(wipToken);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken);
+};
diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
index 934375023ba..691d165c585 100644
--- a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
@@ -17,6 +17,14 @@ const tokenKeys = [
icon: 'cube',
tag: 'type',
},
+ {
+ key: 'tag',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ icon: 'tag',
+ tag: '~tag',
+ },
];
const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
new file mode 100644
index 00000000000..e2f9c03ee65
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -0,0 +1,133 @@
+import DropdownHint from './dropdown_hint';
+import DropdownUser from './dropdown_user';
+import DropdownNonUser from './dropdown_non_user';
+import DropdownEmoji from './dropdown_emoji';
+import NullDropdown from './null_dropdown';
+import DropdownAjaxFilter from './dropdown_ajax_filter';
+import DropdownUtils from './dropdown_utils';
+
+export default class AvailableDropdownMappings {
+ constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) {
+ this.container = container;
+ this.baseEndpoint = baseEndpoint;
+ this.groupsOnly = groupsOnly;
+ this.includeAncestorGroups = includeAncestorGroups;
+ this.includeDescendantGroups = includeDescendantGroups;
+ }
+
+ getAllowedMappings(supportedTokens) {
+ return this.buildMappings(supportedTokens, this.getMappings());
+ }
+
+ buildMappings(supportedTokens, availableMappings) {
+ const allowedMappings = {
+ hint: {
+ reference: null,
+ gl: DropdownHint,
+ element: this.container.querySelector('#js-dropdown-hint'),
+ },
+ };
+
+ supportedTokens.forEach(type => {
+ if (availableMappings[type]) {
+ allowedMappings[type] = availableMappings[type];
+ }
+ });
+
+ return allowedMappings;
+ }
+
+ getMappings() {
+ return {
+ 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: {
+ endpoint: this.getMilestoneEndpoint(),
+ symbol: '%',
+ },
+ element: this.container.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getLabelsEndpoint(),
+ symbol: '~',
+ preprocessing: DropdownUtils.duplicateLabelPreprocessing,
+ },
+ element: this.container.querySelector('#js-dropdown-label'),
+ },
+ 'my-reaction': {
+ reference: null,
+ gl: DropdownEmoji,
+ element: this.container.querySelector('#js-dropdown-my-reaction'),
+ },
+ wip: {
+ reference: null,
+ gl: DropdownNonUser,
+ element: this.container.querySelector('#js-dropdown-wip'),
+ },
+ confidential: {
+ reference: null,
+ gl: DropdownNonUser,
+ element: this.container.querySelector('#js-dropdown-confidential'),
+ },
+ status: {
+ reference: null,
+ gl: NullDropdown,
+ element: this.container.querySelector('#js-dropdown-admin-runner-status'),
+ },
+ type: {
+ reference: null,
+ gl: NullDropdown,
+ element: this.container.querySelector('#js-dropdown-admin-runner-type'),
+ },
+ tag: {
+ reference: null,
+ gl: DropdownAjaxFilter,
+ extraArguments: {
+ endpoint: this.getRunnerTagsEndpoint(),
+ symbol: '~',
+ },
+ element: this.container.querySelector('#js-dropdown-runner-tag'),
+ },
+ };
+ }
+
+ getMilestoneEndpoint() {
+ return `${this.baseEndpoint}/milestones.json`;
+ }
+
+ getLabelsEndpoint() {
+ let endpoint = `${this.baseEndpoint}/labels.json?`;
+
+ if (this.groupsOnly) {
+ endpoint = `${endpoint}only_group_labels=true&`;
+ }
+
+ if (this.includeAncestorGroups) {
+ endpoint = `${endpoint}include_ancestor_groups=true&`;
+ }
+
+ if (this.includeDescendantGroups) {
+ endpoint = `${endpoint}include_descendant_groups=true`;
+ }
+
+ return endpoint;
+ }
+
+ getRunnerTagsEndpoint() {
+ return `${this.baseEndpoint}/admin/runners/tag_list.json`;
+ }
+}
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
index 6b1a934d3fe..19bc3313373 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -66,7 +66,7 @@ export default {
<button
type="button"
class="filtered-search-history-dropdown-item"
- @click="onItemActivated(item.text);"
+ @click="onItemActivated(item.text)"
>
<span>
<span
@@ -88,7 +88,7 @@ export default {
<button
type="button"
class="filtered-search-history-clear-button"
- @click="onRequestClearRecentSearches($event);"
+ @click="onRequestClearRecentSearches($event)"
>
Clear recent searches
</button>
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
new file mode 100644
index 00000000000..b27bb63c220
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -0,0 +1,68 @@
+import createFlash from '../flash';
+import AjaxFilter from '../droplab/plugins/ajax_filter';
+import FilteredSearchDropdown from './filtered_search_dropdown';
+import DropdownUtils from './dropdown_utils';
+import FilteredSearchTokenizer from './filtered_search_tokenizer';
+import { __ } from '~/locale';
+
+export default class DropdownAjaxFilter extends FilteredSearchDropdown {
+ constructor(options = {}) {
+ const { tokenKeys, endpoint, symbol } = options;
+
+ super(options);
+
+ this.tokenKeys = tokenKeys;
+ this.endpoint = endpoint;
+ this.symbol = symbol;
+
+ this.config = {
+ AjaxFilter: this.ajaxFilterConfig(),
+ };
+ }
+
+ ajaxFilterConfig() {
+ return {
+ endpoint: `${gon.relative_url_root || ''}${this.endpoint}`,
+ searchKey: 'search',
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ createFlash(__('An error occurred fetching the dropdown data.'));
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e, selected =>
+ selected.querySelector('.dropdown-light-content').innerText.trim(),
+ );
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ getSearchInput() {
+ const query = DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get());
+
+ let value = lastToken || '';
+
+ if (value[0] === this.symbol) {
+ 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, [AjaxFilter], this.config).init();
+ }
+}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index d5027590bb7..f1e7be6bde1 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -1,54 +1,34 @@
-import Flash from '../flash';
-import AjaxFilter from '../droplab/plugins/ajax_filter';
-import FilteredSearchDropdown from './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
-import DropdownUtils from './dropdown_utils';
-import FilteredSearchTokenizer from './filtered_search_tokenizer';
+import DropdownAjaxFilter from './dropdown_ajax_filter';
-export default class DropdownUser extends FilteredSearchDropdown {
+export default class DropdownUser extends DropdownAjaxFilter {
constructor(options = {}) {
- const { tokenKeys } = options;
- super(options);
- this.config = {
- AjaxFilter: {
- endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
- searchKey: 'search',
- params: {
- active: true,
- group_id: this.getGroupId(),
- project_id: this.getProjectId(),
- current_user: true,
- },
- searchValueFunction: this.getSearchInput.bind(this),
- loadingTemplate: this.loadingTemplate,
- onLoadingFinished: () => {
- this.hideCurrentUser();
- },
- onError() {
- /* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
- /* eslint-enable no-new */
- },
+ super({
+ ...options,
+ endpoint: '/autocomplete/users.json',
+ symbol: '@',
+ });
+ }
+
+ ajaxFilterConfig() {
+ return {
+ ...super.ajaxFilterConfig(),
+ params: {
+ active: true,
+ group_id: this.getGroupId(),
+ project_id: this.getProjectId(),
+ current_user: true,
+ },
+ onLoadingFinished: () => {
+ this.hideCurrentUser();
},
};
- this.tokenKeys = tokenKeys;
}
hideCurrentUser() {
addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
}
- itemClicked(e) {
- super.itemClicked(e, selected =>
- selected.querySelector('.dropdown-light-content').innerText.trim(),
- );
- }
-
- renderContent(forceShowList = false) {
- this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
- super.renderContent(forceShowList);
- }
-
getGroupId() {
return this.input.getAttribute('data-group-id');
}
@@ -56,27 +36,4 @@ export default class DropdownUser extends FilteredSearchDropdown {
getProjectId() {
return this.input.getAttribute('data-project-id');
}
-
- getSearchInput() {
- const query = DropdownUtils.getSearchInput(this.input);
- const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get());
-
- 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, [AjaxFilter], this.config).init();
- }
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 57ec6603d80..cb0a84b490b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,13 +1,9 @@
+import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings';
import _ from 'underscore';
import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
import DropdownUtils from './dropdown_utils';
-import DropdownHint from './dropdown_hint';
-import DropdownEmoji from './dropdown_emoji';
-import DropdownNonUser from './dropdown_non_user';
-import DropdownUser from './dropdown_user';
-import NullDropdown from './null_dropdown';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
@@ -49,96 +45,15 @@ export default class FilteredSearchDropdownManager {
setupMapping() {
const supportedTokens = this.filteredSearchTokenKeys.getKeys();
- const allowedMappings = {
- hint: {
- reference: null,
- gl: DropdownHint,
- element: this.container.querySelector('#js-dropdown-hint'),
- },
- };
- const availableMappings = {
- 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: {
- endpoint: this.getMilestoneEndpoint(),
- symbol: '%',
- },
- element: this.container.querySelector('#js-dropdown-milestone'),
- },
- label: {
- reference: null,
- gl: DropdownNonUser,
- extraArguments: {
- endpoint: this.getLabelsEndpoint(),
- symbol: '~',
- preprocessing: DropdownUtils.duplicateLabelPreprocessing,
- },
- element: this.container.querySelector('#js-dropdown-label'),
- },
- 'my-reaction': {
- reference: null,
- gl: DropdownEmoji,
- element: this.container.querySelector('#js-dropdown-my-reaction'),
- },
- wip: {
- reference: null,
- gl: DropdownNonUser,
- element: this.container.querySelector('#js-dropdown-wip'),
- },
- status: {
- reference: null,
- gl: NullDropdown,
- element: this.container.querySelector('#js-dropdown-admin-runner-status'),
- },
- type: {
- reference: null,
- gl: NullDropdown,
- element: this.container.querySelector('#js-dropdown-admin-runner-type'),
- },
- };
-
- supportedTokens.forEach(type => {
- if (availableMappings[type]) {
- allowedMappings[type] = availableMappings[type];
- }
- });
-
- this.mapping = allowedMappings;
- }
-
- getMilestoneEndpoint() {
- const endpoint = `${this.baseEndpoint}/milestones.json`;
-
- return endpoint;
- }
-
- getLabelsEndpoint() {
- let endpoint = `${this.baseEndpoint}/labels.json?`;
-
- if (this.groupsOnly) {
- endpoint = `${endpoint}only_group_labels=true&`;
- }
-
- if (this.includeAncestorGroups) {
- endpoint = `${endpoint}include_ancestor_groups=true&`;
- }
-
- if (this.includeDescendantGroups) {
- endpoint = `${endpoint}include_descendant_groups=true`;
- }
+ const availableMappings = new AvailableDropdownMappings(
+ this.container,
+ this.baseEndpoint,
+ this.groupsOnly,
+ this.includeAncestorGroups,
+ this.includeDescendantGroups,
+ );
- return endpoint;
+ this.mapping = availableMappings.getAllowedMappings(supportedTokens);
}
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 4a2af02b40a..33c82778c79 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -593,7 +593,7 @@ export default class FilteredSearchManager {
tokens.forEach(token => {
const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
token.key,
- token.value.toLowerCase(),
+ token.value,
);
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index e01dedbb57c..11ed85504ec 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -65,25 +65,27 @@ export default class FilteredSearchTokenKeys {
searchByConditionKeyValue(key, value) {
return (
- this.conditions.find(condition => condition.tokenKey === key && condition.value === value) ||
- null
+ this.conditions.find(
+ condition =>
+ condition.tokenKey === key && condition.value.toLowerCase() === value.toLowerCase(),
+ ) || null
);
}
- addExtraTokensForMergeRequests() {
- const wipToken = {
- key: 'wip',
+ addExtraTokensForIssues() {
+ const confidentialToken = {
+ key: 'confidential',
type: 'string',
param: '',
symbol: '',
- icon: 'admin',
+ icon: 'eye-slash',
tag: 'Yes or No',
lowercaseValueOnSubmit: true,
- uppercaseTokenName: true,
+ uppercaseTokenName: false,
capitalizeTokenValue: true,
};
- this.tokenKeys.push(wipToken);
- this.tokenKeysWithAlternative.push(wipToken);
+ this.tokenKeys.push(confidentialToken);
+ this.tokenKeysWithAlternative.push(confidentialToken);
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index fba31f16d65..7746908714e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,10 +1,6 @@
-import _ from 'underscore';
-import AjaxCache from '~/lib/utils/ajax_cache';
+import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value';
import { objectToQueryString } from '~/lib/utils/common_utils';
-import Flash from '../flash';
import FilteredSearchContainer from './container';
-import UsersCache from '../lib/utils/users_cache';
-import DropdownUtils from './dropdown_utils';
export default class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
@@ -20,21 +16,6 @@ export default class FilteredSearchVisualTokens {
};
}
- /**
- * Returns a computed API endpoint
- * and query string composed of values from endpointQueryParams
- * @param {String} endpoint
- * @param {String} endpointQueryParams
- */
- static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
- if (!endpointQueryParams) {
- return endpoint;
- }
-
- const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
- return `${endpoint}?${queryString}`;
- }
-
static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll(
'.js-visual-token .selectable.selected',
@@ -76,114 +57,15 @@ export default class FilteredSearchVisualTokens {
`;
}
- static setTokenStyle(tokenContainer, backgroundColor, textColor) {
- const token = tokenContainer;
-
- token.style.backgroundColor = backgroundColor;
- token.style.color = textColor;
-
- if (textColor === '#FFFFFF') {
- const removeToken = token.querySelector('.remove-token');
- removeToken.classList.add('inverted');
- }
-
- return token;
- }
-
- static updateLabelTokenColor(tokenValueContainer, tokenValue) {
- const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
- const { baseEndpoint } = filteredSearchInput.dataset;
- const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
- `${baseEndpoint}/labels.json`,
- filteredSearchInput.dataset.endpointQueryParams,
- );
-
- return AjaxCache.retrieve(labelsEndpoint)
- .then(labels => {
- const matchingLabel = (labels || []).find(
- label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
- );
-
- if (!matchingLabel) {
- return;
- }
-
- FilteredSearchVisualTokens.setTokenStyle(
- tokenValueContainer,
- matchingLabel.color,
- matchingLabel.text_color,
- );
- })
- .catch(() => new Flash('An error occurred while fetching label colors.'));
- }
-
- static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
- const username = tokenValue.replace(/^@/, '');
- return (
- UsersCache.retrieve(username)
- .then(user => {
- if (!user) {
- return;
- }
-
- /* eslint-disable no-param-reassign */
- tokenValueContainer.dataset.originalValue = tokenValue;
- tokenValueElement.innerHTML = `
- <img class="avatar s20" src="${user.avatar_url}" alt="">
- ${_.escape(user.name)}
- `;
- /* eslint-enable no-param-reassign */
- })
- // ignore error and leave username in the search bar
- .catch(() => {})
- );
- }
-
- static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
- const container = tokenValueContainer;
- const element = tokenValueElement;
-
- return (
- import(/* webpackChunkName: 'emoji' */ '../emoji')
- .then(Emoji => {
- if (!Emoji.isEmojiNameValid(tokenValue)) {
- return;
- }
-
- container.dataset.originalValue = tokenValue;
- element.innerHTML = Emoji.glEmojiTag(tokenValue);
- })
- // ignore error and leave emoji name in the search bar
- .catch(() => {})
- );
- }
-
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
+ const tokenType = tokenName.toLowerCase();
const tokenValueContainer = parentElement.querySelector('.value-container');
const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
- if (tokenValue === 'none' || tokenValue === 'any') {
- return;
- }
+ const visualTokenValue = new VisualTokenValue(tokenValue, tokenType);
- const tokenType = tokenName.toLowerCase();
-
- if (tokenType === 'label') {
- FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
- } else if (tokenType === 'author' || tokenType === 'assignee') {
- FilteredSearchVisualTokens.updateUserTokenAppearance(
- tokenValueContainer,
- tokenValueElement,
- tokenValue,
- );
- } else if (tokenType === 'my-reaction') {
- FilteredSearchVisualTokens.updateEmojiTokenAppearance(
- tokenValueContainer,
- tokenValueElement,
- tokenValue,
- );
- }
+ visualTokenValue.render(tokenValueContainer, tokenValueElement);
}
static addVisualTokenElement(name, value, options = {}) {
@@ -318,6 +200,21 @@ export default class FilteredSearchVisualTokens {
}
}
+ /**
+ * Returns a computed API endpoint
+ * and query string composed of values from endpointQueryParams
+ * @param {String} endpoint
+ * @param {String} endpointQueryParams
+ */
+ static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
+ if (!endpointQueryParams) {
+ return endpoint;
+ }
+
+ const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
+ return `${endpoint}?${queryString}`;
+ }
+
static editToken(token) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index b494b7e2de0..fd61030eb13 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -60,52 +60,52 @@ export const conditions = [
{
url: 'assignee_id=None',
tokenKey: 'assignee',
- value: 'none',
+ value: 'None',
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
- value: 'any',
+ value: 'Any',
},
{
url: 'milestone_title=None',
tokenKey: 'milestone',
- value: 'none',
+ value: 'None',
},
{
url: 'milestone_title=Any',
tokenKey: 'milestone',
- value: 'any',
+ value: 'Any',
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
- value: 'upcoming',
+ value: 'Upcoming',
},
{
url: 'milestone_title=%23started',
tokenKey: 'milestone',
- value: 'started',
+ value: 'Started',
},
{
url: 'label_name[]=None',
tokenKey: 'label',
- value: 'none',
+ value: 'None',
},
{
url: 'label_name[]=Any',
- tokenKey: 'any',
- value: 'any',
+ tokenKey: 'label',
+ value: 'Any',
},
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
- value: 'none',
+ value: 'None',
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
- value: 'any',
+ value: 'Any',
},
];
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
new file mode 100644
index 00000000000..7f6f41c18f7
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -0,0 +1,125 @@
+import _ from 'underscore';
+import FilteredSearchContainer from '~/filtered_search/container';
+import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import DropdownUtils from '~/filtered_search/dropdown_utils';
+import Flash from '~/flash';
+import UsersCache from '~/lib/utils/users_cache';
+
+export default class VisualTokenValue {
+ constructor(tokenValue, tokenType) {
+ this.tokenValue = tokenValue;
+ this.tokenType = tokenType;
+ }
+
+ render(tokenValueContainer, tokenValueElement) {
+ const { tokenType } = this;
+
+ if (['none', 'any'].includes(tokenType)) {
+ return;
+ }
+
+ if (tokenType === 'label') {
+ this.updateLabelTokenColor(tokenValueContainer);
+ } else if (tokenType === 'author' || tokenType === 'assignee') {
+ this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement);
+ } else if (tokenType === 'my-reaction') {
+ this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement);
+ }
+ }
+
+ updateUserTokenAppearance(tokenValueContainer, tokenValueElement) {
+ const { tokenValue } = this;
+ const username = this.tokenValue.replace(/^@/, '');
+
+ return (
+ UsersCache.retrieve(username)
+ .then(user => {
+ if (!user) {
+ return;
+ }
+
+ /* eslint-disable no-param-reassign */
+ tokenValueContainer.dataset.originalValue = tokenValue;
+ tokenValueElement.innerHTML = `
+ <img class="avatar s20" src="${user.avatar_url}" alt="">
+ ${_.escape(user.name)}
+ `;
+ /* eslint-enable no-param-reassign */
+ })
+ // ignore error and leave username in the search bar
+ .catch(() => {})
+ );
+ }
+
+ updateLabelTokenColor(tokenValueContainer) {
+ const { tokenValue } = this;
+ const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
+ const { baseEndpoint } = filteredSearchInput.dataset;
+ const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
+ `${baseEndpoint}/labels.json`,
+ filteredSearchInput.dataset.endpointQueryParams,
+ );
+
+ return AjaxCache.retrieve(labelsEndpoint)
+ .then(labels => {
+ const matchingLabel = (labels || []).find(
+ label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue,
+ );
+
+ if (!matchingLabel) {
+ return;
+ }
+
+ VisualTokenValue.setTokenStyle(
+ tokenValueContainer,
+ matchingLabel.color,
+ matchingLabel.text_color,
+ );
+ })
+ .catch(() => new Flash('An error occurred while fetching label colors.'));
+ }
+
+ static setTokenStyle(tokenValueContainer, backgroundColor, textColor) {
+ const token = tokenValueContainer;
+
+ token.style.backgroundColor = backgroundColor;
+ token.style.color = textColor;
+
+ if (textColor === '#FFFFFF') {
+ const removeToken = token.querySelector('.remove-token');
+ removeToken.classList.add('inverted');
+ }
+
+ return token;
+ }
+
+ updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement) {
+ const container = tokenValueContainer;
+ const element = tokenValueElement;
+ const value = this.tokenValue;
+
+ return (
+ import(/* webpackChunkName: 'emoji' */ '../emoji')
+ .then(Emoji => {
+ Emoji.initEmojiMap()
+ .then(() => {
+ if (!Emoji.isEmojiNameValid(value)) {
+ return;
+ }
+
+ container.dataset.originalValue = value;
+ element.innerHTML = Emoji.glEmojiTag(value);
+ })
+ // ignore error and leave emoji name in the search bar
+ .catch(err => {
+ throw err;
+ });
+ })
+ // ignore error and leave emoji name in the search bar
+ .catch(importError => {
+ throw importError;
+ })
+ );
+ }
+}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 3ac00c51df4..2b6af9060d1 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -24,6 +24,9 @@ export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
+const setHeaderHeight = () => {
+ headerHeight = sidebar.offsetTop;
+};
export const isSidebarCollapsed = () =>
sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
@@ -186,7 +189,7 @@ export default () => {
});
}
- headerHeight = document.querySelector('.nav-sidebar').offsetTop;
+ requestIdleCallback(setHeaderHeight);
items.forEach(el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 63531f1f246..968e255e1fc 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -47,6 +47,12 @@ export default {
}
eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+
+ // As we init it through requestIdleCallback it could be that the dropdown is already open
+ const namespaceDropdown = document.getElementById(`nav-${this.namespace}-dropdown`);
+ if (namespaceDropdown && namespaceDropdown.classList.contains('show')) {
+ this.dropdownOpenHandler();
+ }
},
beforeDestroy() {
eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 2cbc7c7077b..42d14b65b3a 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -82,8 +82,14 @@ export default {
<li class="frequent-items-list-item-container">
<a :href="webUrl" class="clearfix">
<div class="frequent-items-item-avatar-container">
- <img v-if="hasAvatar" :src="avatarUrl" class="avatar s32" />
- <identicon v-else :entity-id="itemId" :entity-name="itemName" size-class="s32" />
+ <img v-if="hasAvatar" :src="avatarUrl" class="avatar rect-avatar s32" />
+ <identicon
+ v-else
+ :entity-id="itemId"
+ :entity-name="itemName"
+ size-class="s32"
+ class="rect-avatar"
+ />
</div>
<div class="frequent-items-item-metadata-container">
<div :title="itemName" class="frequent-items-item-title" v-html="highlightedItemName"></div>
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index 5157ff211dc..6263acbab8e 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -17,7 +17,7 @@ const frequentItemDropdowns = [
},
];
-document.addEventListener('DOMContentLoaded', () => {
+const initFrequentItemDropdowns = () => {
frequentItemDropdowns.forEach(dropdown => {
const { namespace, key } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
@@ -66,4 +66,8 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
});
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ requestIdleCallback(initFrequentItemDropdowns);
});
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index c14eb936930..50ea13edf63 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -195,11 +195,15 @@ class GfmAutoComplete {
title += ` (${m.count})`;
}
+ const GROUP_TYPE = 'Group';
+
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+
+ const rectAvatarClass = m.type === GROUP_TYPE ? 'rect-avatar' : '';
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>`;
+ }" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
return {
username: m.username,
@@ -221,13 +225,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
- // eslint-disable-next-line no-template-curly-in-string
- insertTpl: '${atwho-at}${id}',
+ insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
+ skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
@@ -238,6 +242,7 @@ class GfmAutoComplete {
return {
id: i.iid,
title: sanitize(i.title),
+ reference: i.reference,
search: `${i.iid} ${i.title}`,
};
});
@@ -256,7 +261,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Milestones.template;
+ tmpl = GfmAutoComplete.Milestones.templateFunction(value.title);
}
return tmpl;
},
@@ -287,13 +292,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
- // eslint-disable-next-line no-template-curly-in-string
- insertTpl: '${atwho-at}${id}',
+ insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
+ skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
@@ -304,6 +309,7 @@ class GfmAutoComplete {
return {
id: m.iid,
title: sanitize(m.title),
+ reference: m.reference,
search: `${m.iid} ${m.title}`,
};
});
@@ -323,7 +329,7 @@ class GfmAutoComplete {
searchKey: 'search',
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
- let tmpl = GfmAutoComplete.Labels.template;
+ let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title);
if (GfmAutoComplete.isLoading(value)) {
tmpl = GfmAutoComplete.Loading.template;
}
@@ -397,7 +403,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
@@ -481,9 +487,15 @@ class GfmAutoComplete {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
import(/* webpackChunkName: 'emoji' */ './emoji')
- .then(({ validEmojiNames, glEmojiTag }) => {
- this.loadData($input, at, validEmojiNames);
- GfmAutoComplete.glEmojiTag = glEmojiTag;
+ .then(({ initEmojiMap, getValidEmojiNames, glEmojiTag }) => {
+ initEmojiMap()
+ .then(() => {
+ this.loadData($input, at, getValidEmojiNames());
+ GfmAutoComplete.glEmojiTag = glEmojiTag;
+ })
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
})
.catch(() => {
this.isLoadingData[at] = false;
@@ -588,20 +600,27 @@ GfmAutoComplete.Members = {
},
};
GfmAutoComplete.Labels = {
- template:
- // eslint-disable-next-line no-template-curly-in-string
- '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+ templateFunction(color, title) {
+ return `<li><span class="dropdown-label-box" style="background: ${_.escape(
+ color,
+ )}"></span> ${_.escape(title)}</li>`;
+ },
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
- templateFunction(id, title) {
- return `<li><small>${id}</small> ${_.escape(title)}</li>`;
+ insertTemplateFunction(value) {
+ // eslint-disable-next-line no-template-curly-in-string
+ return value.reference || '${atwho-at}${id}';
+ },
+ templateFunction({ id, title, reference }) {
+ return `<li><small>${reference || id}</small> ${_.escape(title)}</li>`;
},
};
// Milestones
GfmAutoComplete.Milestones = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li>${title}</li>',
+ templateFunction(title) {
+ return `<li>${_.escape(title)}</li>`;
+ },
};
GfmAutoComplete.Loading = {
template:
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a8ac2f510a4..27d8669b256 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -656,23 +656,7 @@ GitLabDropdown = (function() {
if (this.options.renderMenu) {
return this.options.renderMenu(html);
} else {
- var ul = document.createElement('ul');
-
- for (var i = 0; i < html.length; i += 1) {
- var el = html[i];
-
- if (el instanceof $) {
- el = el.get(0);
- }
-
- if (typeof el === 'string') {
- ul.innerHTML += el;
- } else {
- ul.appendChild(el);
- }
- }
-
- return ul;
+ return $('<ul>').append(html);
}
};
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index f842d2d74db..f5e2e46237f 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -51,8 +51,6 @@ export default class GLForm {
// form and textarea event listeners
this.addEventListeners();
addMarkdownListeners(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
this.form.show();
if (this.isAutosizeable) this.setupAutosize();
}
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 688bd37cc56..d5130cd331d 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -88,7 +88,7 @@ export default {
</div>
<div
:class="{ 'content-loading': group.isChildrenLoading }"
- class="avatar-container s24 d-none d-sm-flex"
+ class="avatar-container rect-avatar s24 d-none d-sm-flex"
>
<a :href="group.relativePath" class="no-expand">
<img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s24" />
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 2049760fe29..bdadbb1bb2a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,93 +4,97 @@ import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- $select.select2({
- placeholder: 'Search for a group',
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $select.select2({
+ placeholder: 'Search for a group',
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${
- object.full_name
- }</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return object.full_name;
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${
+ object.full_name
+ }</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return object.full_name;
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 2fa7a219ea8..3d846310008 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -23,7 +23,7 @@ export default function initTodoToggle() {
});
}
-document.addEventListener('DOMContentLoaded', () => {
+function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
@@ -72,4 +72,8 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ requestIdleCallback(initStatusTriggers);
});
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index a1f66ff764d..7c769ab7fa0 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -45,7 +45,7 @@ export default {
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
- @click.prevent="changedActivityView($event, $options.activityBarViews.edit);"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.edit)"
>
<icon name="code" />
</button>
@@ -62,7 +62,7 @@ export default {
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-review-mode"
- @click.prevent="changedActivityView($event, $options.activityBarViews.review);"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.review)"
>
<icon name="file-modified" />
</button>
@@ -79,7 +79,7 @@ export default {
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
- @click.prevent="changedActivityView($event, $options.activityBarViews.commit);"
+ @click.prevent="changedActivityView($event, $options.activityBarViews.commit)"
>
<icon name="commit" />
</button>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 5119dbf32eb..11d5d9639b6 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -44,7 +44,7 @@ export default {
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
<strong class="mr-2"> {{ activeFile.path }} </strong>
- <changed-file-icon :file="activeFile" class="ml-0" />
+ <changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
<button
v-if="!isStaged"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index a1094570275..4f1260de0bc 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -87,7 +87,7 @@ export default {
},
},
discardModalText: __(
- "You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
+ "You will lose all the unstaged changes you've made in this project. This action cannot be undone.",
),
};
</script>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index 6f1ded91753..00b2d236da3 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -111,8 +111,8 @@ export default {
name="commit-message"
@scroll="handleScroll"
@input="onInput"
- @focus="updateIsFocused(true);"
- @blur="updateIsFocused(false);"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
@keydown.ctrl.enter="onCtrlEnter"
@keydown.meta.enter="onCtrlEnter"
>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 3525084b1cb..2b44438f849 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -65,7 +65,7 @@ export default {
:disabled="disabled"
type="radio"
name="commit-action"
- @change="updateCommitAction($event.target.value);"
+ @change="updateCommitAction($event.target.value)"
/>
<span class="prepend-left-10">
<span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot>
@@ -76,7 +76,7 @@ export default {
:placeholder="newBranchName"
type="text"
class="form-control monospace"
- @input="updateBranchName($event.target.value);"
+ @input="updateBranchName($event.target.value)"
/>
</div>
</fieldset>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index 02c2004d495..09c9d135614 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -48,7 +48,7 @@ export default {
data-container="body"
data-boundary="viewport"
data-placement="bottom"
- @click.stop.prevent="stageChange(path);"
+ @click.stop.prevent="stageChange(path)"
>
<icon :size="16" name="mobile-issue-close" class="ml-auto mr-auto" />
</button>
@@ -70,9 +70,9 @@ export default {
:header-title-text="modalTitle"
:footer-primary-button-text="__('Discard changes')"
footer-primary-button-variant="danger"
- @submit="discardFileChanges(path);"
+ @submit="discardFileChanges(path)"
>
- {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
+ {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
</gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index ce41fcdb087..0567ef54ff3 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -33,7 +33,7 @@ export default {
data-container="body"
data-boundary="viewport"
data-placement="bottom"
- @click.stop.prevent="unstageChange(path);"
+ @click.stop.prevent="unstageChange(path)"
>
<icon :size="16" name="redo" class="ml-auto mr-auto" />
</button>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 5f99261ec39..732fa0786b0 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -40,7 +40,7 @@ export default {
'is-active': viewer === $options.viewerTypes.mr,
}"
href="#"
- @click.prevent="changeMode($options.viewerTypes.mr);"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
>
<strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong>
<span class="dropdown-menu-inner-content">
@@ -54,7 +54,7 @@ export default {
'is-active': viewer === $options.viewerTypes.diff,
}"
href="#"
- @click.prevent="changeMode($options.viewerTypes.diff);"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
deleted file mode 100644
index bb391912572..00000000000
--- a/app/assets/javascripts/ide/components/file_finder/index.vue
+++ /dev/null
@@ -1,227 +0,0 @@
-<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import VirtualList from 'vue-virtual-scroll-list';
-import Item from './item.vue';
-import router from '../../ide_router';
-import {
- MAX_FILE_FINDER_RESULTS,
- FILE_FINDER_ROW_HEIGHT,
- FILE_FINDER_EMPTY_ROW_HEIGHT,
-} from '../../constants';
-import {
- UP_KEY_CODE,
- DOWN_KEY_CODE,
- ENTER_KEY_CODE,
- ESC_KEY_CODE,
-} from '../../../lib/utils/keycodes';
-
-export default {
- components: {
- Item,
- VirtualList,
- },
- data() {
- return {
- focusedIndex: 0,
- searchText: '',
- mouseOver: false,
- cancelMouseOver: false,
- };
- },
- computed: {
- ...mapGetters(['allBlobs']),
- ...mapState(['fileFindVisible', 'loading']),
- filteredBlobs() {
- const searchText = this.searchText.trim();
-
- if (searchText === '') {
- return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
- }
-
- return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
- key: 'path',
- maxResults: MAX_FILE_FINDER_RESULTS,
- });
- },
- filteredBlobsLength() {
- return this.filteredBlobs.length;
- },
- listShowCount() {
- return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
- },
- listHeight() {
- return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
- },
- showClearInputButton() {
- return this.searchText.trim() !== '';
- },
- },
- watch: {
- fileFindVisible() {
- this.$nextTick(() => {
- if (!this.fileFindVisible) {
- this.searchText = '';
- } else {
- this.focusedIndex = 0;
-
- if (this.$refs.searchInput) {
- this.$refs.searchInput.focus();
- }
- }
- });
- },
- searchText() {
- this.focusedIndex = 0;
- },
- focusedIndex() {
- if (!this.mouseOver) {
- this.$nextTick(() => {
- const el = this.$refs.virtualScrollList.$el;
- const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
- const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
-
- if (this.focusedIndex === 0) {
- // if index is the first index, scroll straight to start
- el.scrollTop = 0;
- } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
- // if index is the last index, scroll to the end
- el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
- } else if (scrollTop >= bottom + el.scrollTop) {
- // if element is off the bottom of the scroll list, scroll down one item
- el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
- } else if (scrollTop < el.scrollTop) {
- // if element is off the top of the scroll list, scroll up one item
- el.scrollTop = scrollTop;
- }
- });
- }
- },
- },
- methods: {
- ...mapActions(['toggleFileFinder']),
- clearSearchInput() {
- this.searchText = '';
-
- this.$nextTick(() => {
- this.$refs.searchInput.focus();
- });
- },
- onKeydown(e) {
- switch (e.keyCode) {
- case UP_KEY_CODE:
- e.preventDefault();
- this.mouseOver = false;
- this.cancelMouseOver = true;
- if (this.focusedIndex > 0) {
- this.focusedIndex -= 1;
- } else {
- this.focusedIndex = this.filteredBlobsLength - 1;
- }
- break;
- case DOWN_KEY_CODE:
- e.preventDefault();
- this.mouseOver = false;
- this.cancelMouseOver = true;
- if (this.focusedIndex < this.filteredBlobsLength - 1) {
- this.focusedIndex += 1;
- } else {
- this.focusedIndex = 0;
- }
- break;
- default:
- break;
- }
- },
- onKeyup(e) {
- switch (e.keyCode) {
- case ENTER_KEY_CODE:
- this.openFile(this.filteredBlobs[this.focusedIndex]);
- break;
- case ESC_KEY_CODE:
- this.toggleFileFinder(false);
- break;
- default:
- break;
- }
- },
- openFile(file) {
- this.toggleFileFinder(false);
- router.push(`/project${file.url}`);
- },
- onMouseOver(index) {
- if (!this.cancelMouseOver) {
- this.mouseOver = true;
- this.focusedIndex = index;
- }
- },
- onMouseMove(index) {
- this.cancelMouseOver = false;
- this.onMouseOver(index);
- },
- },
-};
-</script>
-
-<template>
- <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false);">
- <div class="dropdown-menu diff-file-changes ide-file-finder show">
- <div class="dropdown-input">
- <input
- ref="searchInput"
- v-model="searchText"
- :placeholder="__('Search files')"
- type="search"
- class="dropdown-input-field"
- autocomplete="off"
- @keydown="onKeydown($event);"
- @keyup="onKeyup($event);"
- />
- <i
- :class="{
- hidden: showClearInputButton,
- }"
- aria-hidden="true"
- class="fa fa-search dropdown-input-search"
- ></i>
- <i
- :aria-label="__('Clear search input')"
- :class="{
- show: showClearInputButton,
- }"
- role="button"
- class="fa fa-times dropdown-input-clear"
- @click="clearSearchInput"
- ></i>
- </div>
- <div>
- <virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
- <template v-if="filteredBlobsLength">
- <li v-for="(file, index) in filteredBlobs" :key="file.key">
- <item
- :file="file"
- :search-text="searchText"
- :focused="index === focusedIndex"
- :index="index"
- class="disable-hover"
- @click="openFile"
- @mouseover="onMouseOver"
- @mousemove="onMouseMove"
- />
- </li>
- </template>
- <li v-else class="dropdown-menu-empty-item">
- <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
- <template v-if="loading">
- {{ __('Loading...') }}
- </template>
- <template v-else>
- {{ __('No files found.') }}
- </template>
- </div>
- </li>
- </virtual-list>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
deleted file mode 100644
index 83e80d50aff..00000000000
--- a/app/assets/javascripts/ide/components/file_finder/item.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import FileIcon from '../../../vue_shared/components/file_icon.vue';
-import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
-
-const MAX_PATH_LENGTH = 60;
-
-export default {
- components: {
- ChangedFileIcon,
- FileIcon,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- focused: {
- type: Boolean,
- required: true,
- },
- searchText: {
- type: String,
- required: true,
- },
- index: {
- type: Number,
- required: true,
- },
- },
- computed: {
- pathWithEllipsis() {
- const { path } = this.file;
-
- return path.length < MAX_PATH_LENGTH
- ? path
- : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
- },
- nameSearchTextOccurences() {
- return fuzzaldrinPlus.match(this.file.name, this.searchText);
- },
- pathSearchTextOccurences() {
- return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
- },
- },
- methods: {
- clickRow() {
- this.$emit('click', this.file);
- },
- mouseOverRow() {
- this.$emit('mouseover', this.index);
- },
- mouseMove() {
- this.$emit('mousemove', this.index);
- },
- },
-};
-</script>
-
-<template>
- <button
- :class="{
- 'is-focused': focused,
- }"
- type="button"
- class="diff-changed-file"
- @click.prevent="clickRow"
- @mouseover="mouseOverRow"
- @mousemove="mouseMove"
- >
- <file-icon
- :file-name="file.name"
- :size="16"
- css-classes="diff-file-changed-icon append-right-8"
- />
- <span class="diff-changed-file-content append-right-8">
- <strong class="diff-changed-file-name">
- <span
- v-for="(char, charIndex) in file.name.split('')"
- :key="charIndex + char"
- :class="{
- highlighted: nameSearchTextOccurences.indexOf(charIndex) >= 0,
- }"
- v-text="char"
- >
- </span>
- </strong>
- <span class="diff-changed-file-path prepend-top-5">
- <span
- v-for="(char, charIndex) in pathWithEllipsis.split('')"
- :key="charIndex + char"
- :class="{
- highlighted: pathSearchTextOccurences.indexOf(charIndex) >= 0,
- }"
- v-text="char"
- >
- </span>
- </span>
- </span>
- <span v-if="file.changed || file.tempFile" class="diff-changed-stats">
- <changed-file-icon :file="file" />
- </span>
- </button>
-</template>
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 414ea9c7d4d..343e0cca672 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -91,7 +91,7 @@ export default {
<gl-loading-icon v-if="showLoading" :size="2" />
<ul v-else>
<li v-for="(item, index) in outputData" :key="index">
- <button type="button" @click="clickItem(item);">{{ item.name }}</button>
+ <button type="button" @click="clickItem(item)">{{ item.name }}</button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index caec8779cac..9894ebb0624 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,20 +1,17 @@
<script>
import Vue from 'vue';
-import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
+import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
-import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
-const originalStopCallback = Mousetrap.stopCallback;
-
export default {
components: {
NewModal,
@@ -42,21 +39,18 @@ export default {
'emptyStateSvgPath',
'currentProjectId',
'errorMessage',
+ 'loading',
+ ]),
+ ...mapGetters([
+ 'activeFile',
+ 'hasChanges',
+ 'someUncommittedChanges',
+ 'isCommitModeActive',
+ 'allBlobs',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
-
- Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
- if (e.preventDefault) {
- e.preventDefault();
- }
-
- this.toggleFileFinder(!this.fileFindVisible);
- });
-
- Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
@@ -70,17 +64,8 @@ export default {
});
return returnValue;
},
- mousetrapStopCallback(e, el, combo) {
- if (
- (combo === 't' && el.classList.contains('dropdown-input-field')) ||
- el.classList.contains('inputarea')
- ) {
- return true;
- } else if (combo === 'command+p' || combo === 'ctrl+p') {
- return false;
- }
-
- return originalStopCallback(e, el, combo);
+ openFile(file) {
+ this.$router.push(`/project${file.url}`);
},
},
};
@@ -90,7 +75,14 @@ export default {
<article class="ide position-relative d-flex flex-column align-items-stretch">
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
- <find-file v-show="fileFindVisible" />
+ <find-file
+ v-show="fileFindVisible"
+ :files="allBlobs"
+ :visible="fileFindVisible"
+ :loading="loading"
+ @toggle="toggleFileFinder"
+ @click="openFile"
+ />
<ide-sidebar />
<div class="multi-file-edit-pane">
<template v-if="activeFile">
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index e2e0acc22b1..ce577ae85b0 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -84,7 +84,7 @@ export default {
<button
type="button"
class="p-0 border-0 h-50"
- @click="openRightPane($options.rightSidebarViews.pipelines);"
+ @click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
v-tooltip
@@ -107,16 +107,23 @@ export default {
class="commit-sha"
>{{ lastCommit.short_id }}</a
>
- by {{ lastCommit.author_name }}
+ by
+ <user-avatar-image
+ css-classes="ide-status-avatar"
+ :size="18"
+ :img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
+ :img-alt="lastCommit.author_name"
+ :tooltip-text="lastCommit.author_name"
+ />
+ {{ lastCommit.author_name }}
<time
v-tooltip
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
+ >{{ lastCommitFormatedAge }}</time
>
- {{ lastCommitFormatedAge }}
- </time>
</div>
<div v-if="file" class="ide-status-file">{{ file.name }}</div>
<div v-if="file" class="ide-status-file">{{ file.eol }}</div>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 9fc21adae7c..f93496132a4 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -43,7 +43,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0 mr-3 qa-new-file"
icon="doc-new"
- @click="openNewEntryModal({ type: 'blob' });"
+ @click="openNewEntryModal({ type: 'blob' })"
/>
<upload
:show-label="false"
@@ -56,7 +56,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
- @click="openNewEntryModal({ type: 'tree' });"
+ @click="openNewEntryModal({ type: 'tree' })"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index e8fe5fc696d..7710bfb49ec 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -75,7 +75,7 @@ export default {
<template>
<div class="ide-pipeline build-page d-flex flex-column flex-fill">
<header class="ide-job-header d-flex align-items-center">
- <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null);">
+ <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)">
<icon name="chevron-left" /> {{ __('View jobs') }}
</button>
</header>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index ac2b0eddfb4..2d55ffb3c65 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -84,7 +84,7 @@ export default {
:placeholder="__('Search merge requests')"
@focus="onSearchFocus"
@input="searchMergeRequests"
- @removeToken="setSearchType(null);"
+ @removeToken="setSearchType(null)"
/>
<icon :size="18" name="search" class="input-icon" />
</div>
@@ -102,7 +102,7 @@ export default {
<button
type="button"
class="btn-link d-flex align-items-center"
- @click.stop="setSearchType(searchType);"
+ @click.stop="setSearchType(searchType)"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon :size="18" name="search" />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index a50d729036f..593a9162a06 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,7 +1,6 @@
<script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
-import newModal from './modal.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
@@ -9,7 +8,6 @@ import { modalTypes } from '../../constants';
export default {
components: {
icon,
- newModal,
upload,
ItemButton,
},
@@ -73,7 +71,7 @@ export default {
:aria-label="__('Create new file or directory')"
type="button"
class="rounded border-0 d-flex ide-entry-dropdown-toggle"
- @click.stop="openDropdown();"
+ @click.stop="openDropdown()"
>
<icon name="ellipsis_v" /> <icon name="arrow-down" />
</button>
@@ -85,7 +83,7 @@ export default {
class="d-flex"
icon="doc-new"
icon-classes="mr-2"
- @click="createNewItem('blob');"
+ @click="createNewItem('blob')"
/>
</li>
<li><upload :path="path" @create="createTempEntry" /></li>
@@ -95,7 +93,7 @@ export default {
class="d-flex"
icon="folder-new"
icon-classes="mr-2"
- @click="createNewItem($options.modalTypes.tree);"
+ @click="createNewItem($options.modalTypes.tree)"
/>
</li>
<li class="divider"></li>
@@ -106,7 +104,7 @@ export default {
class="d-flex"
icon="pencil"
icon-classes="mr-2"
- @click="createNewItem($options.modalTypes.rename);"
+ @click="createNewItem($options.modalTypes.rename)"
/>
</li>
<li>
@@ -115,7 +113,7 @@ export default {
class="d-flex"
icon="remove"
icon-classes="mr-2"
- @click="deleteEntry(path);"
+ @click="deleteEntry(path)"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 63cbf41b89b..ba6bbdfef4b 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
-import { __ } from '~/locale';
+import flash from '~/flash';
+import { __, sprintf, s__ } from '~/locale';
import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
@@ -15,15 +16,17 @@ export default {
};
},
computed: {
- ...mapState(['entryModal']),
+ ...mapState(['entries', 'entryModal']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: {
get() {
+ const entryPath = this.entryModal.entry.path;
+
if (this.entryModal.type === modalTypes.rename) {
- return this.name || this.entryModal.entry.name;
+ return this.name || entryPath;
}
- return this.name || (this.entryModal.path !== '' ? `${this.entryModal.path}/` : '');
+ return this.name || (entryPath ? `${entryPath}/` : '');
},
set(val) {
this.name = val;
@@ -51,18 +54,51 @@ export default {
return __('Create file');
},
- isCreatingNew() {
- return this.entryModal.type !== modalTypes.rename;
+ isCreatingNewFile() {
+ return this.entryModal.type === 'blob';
+ },
+ placeholder() {
+ return this.isCreatingNewFile ? 'dir/file_name' : 'dir/';
},
},
methods: {
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
if (this.entryModal.type === modalTypes.rename) {
- this.renameEntry({
- path: this.entryModal.entry.path,
- name: this.entryName,
- });
+ if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
+ flash(
+ sprintf(s__('The name %{entryName} is already taken in this directory.'), {
+ entryName: this.entryName,
+ }),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ } else {
+ let parentPath = this.entryName.split('/');
+ const entryName = parentPath.pop();
+ parentPath = parentPath.join('/');
+
+ const createPromise =
+ parentPath && !this.entries[parentPath]
+ ? this.createTempEntry({ name: parentPath, type: 'tree' })
+ : Promise.resolve();
+
+ createPromise
+ .then(() =>
+ this.renameEntry({
+ path: this.entryModal.entry.path,
+ name: entryName,
+ entryPath: null,
+ parentPath,
+ }),
+ )
+ .catch(() =>
+ flash(__('Error creating a new path'), 'alert', document, null, false, true),
+ );
+ }
} else {
this.createTempEntry({
name: this.name,
@@ -79,7 +115,14 @@ export default {
$('#ide-new-entry').modal('toggle');
},
focusInput() {
+ const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null;
+ const inputValue = this.$refs.fieldName.value;
+
this.$refs.fieldName.focus();
+
+ if (name) {
+ this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
+ }
},
closedModal() {
this.name = '';
@@ -107,14 +150,17 @@ export default {
v-model="entryName"
type="text"
class="form-control qa-full-file-path"
- placeholder="/dir/file_name"
+ :placeholder="placeholder"
/>
- <ul v-if="isCreatingNew" class="prepend-top-default list-inline qa-template-list">
+ <ul
+ v-if="isCreatingNewFile"
+ class="file-templates prepend-top-default list-inline qa-template-list"
+ >
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
<button
type="button"
class="btn btn-missing p-1 pr-2 pl-2"
- @click="createFromTemplate(template);"
+ @click="createFromTemplate(template)"
>
{{ template.name }}
</button>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 7a57ccf2dd3..2e6bd85feec 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -122,7 +122,7 @@ export default {
data-placement="left"
class="ide-sidebar-link is-right"
type="button"
- @click="clickTab($event, tab);"
+ @click="clickTab($event, tab)"
>
<icon :size="16" :name="tab.icon" />
</button>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index c13d3ec094b..94a9e87369c 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -219,7 +219,7 @@ export default {
<a
href="javascript:void(0);"
role="button"
- @click.prevent="setFileViewMode({ file, viewMode: 'editor' });"
+ @click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
>
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
@@ -233,7 +233,7 @@ export default {
<a
href="javascript:void(0);"
role="button"
- @click.prevent="setFileViewMode({ file, viewMode: 'preview' });"
+ @click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
>
{{ file.previewMode.previewTitle }}
</a>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 4b87b83db8a..f6aa2295844 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -74,7 +74,7 @@ export default {
active: tab.active,
disabled: tab.pending,
}"
- @click="clickFile(tab);"
+ @click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
@@ -88,7 +88,7 @@ export default {
:disabled="tab.pending"
type="button"
class="multi-file-tab-close"
- @click.stop.prevent="closeFile(tab);"
+ @click.stop.prevent="closeFile(tab)"
>
<icon v-if="!showChangedIcon" :size="12" name="close" />
<changed-file-icon v-else :file="tab" />
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index a89de56ab5c..7277fcb7617 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -78,8 +78,8 @@ export default {
:min-size="minSize"
:max-size="$options.maxSize"
:side="side === 'right' ? 'left' : 'right'"
- @resize-start="setResizingStatus(true);"
- @resize-end="setResizingStatus(false);"
+ @resize-start="setResizingStatus(true)"
+ @resize-end="setResizingStatus(false)"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue
index f58e08c2cc9..de3e71dad92 100644
--- a/app/assets/javascripts/ide/components/shared/tokened_input.vue
+++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue
@@ -76,8 +76,8 @@ export default {
<button
class="selectable btn-blank"
type="button"
- @click.stop="removeToken(token);"
- @keyup.delete="removeToken(token);"
+ @click.stop="removeToken(token)"
+ @keyup.delete="removeToken(token)"
>
<div class="value-container rounded">
<div class="value">{{ token.label }}</div>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 09245ed0296..7c560c89695 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -1,8 +1,3 @@
-// Fuzzy file finder
-export const MAX_FILE_FINDER_RESULTS = 40;
-export const FILE_FINDER_ROW_HEIGHT = 55;
-export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
-
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
// Commit message textarea
@@ -29,6 +24,22 @@ export const diffModes = {
mode_changed: 'mode_changed',
};
+export const diffViewerModes = Object.freeze({
+ not_diffable: 'not_diffable',
+ no_preview: 'no_preview',
+ added: 'added',
+ deleted: 'deleted',
+ renamed: 'renamed',
+ mode_changed: 'mode_changed',
+ text: 'text',
+ image: 'image',
+});
+
+export const diffViewerErrors = Object.freeze({
+ too_large: 'too_large',
+ stored_externally: 'server_side_but_stored_externally',
+});
+
export const rightSidebarViews = {
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 3f6101e58f4..229ef168926 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -73,7 +73,7 @@ router.beforeEach((to, from, next) => {
projectId: to.params.project,
})
.then(() => {
- const basePath = to.params[0] || '';
+ const basePath = to.params.pathMatch || '';
const projectId = `${to.params.namespace}/${to.params.project}`;
const branchId = to.params.branchid;
const mergeRequestId = to.params.mrid;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 6351948f750..cdfebd19fa4 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -6,17 +6,25 @@ import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
import { parseBoolean } from '../lib/utils/common_utils';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
Vue.use(Translate);
/**
+ * Function that receives the default store and returns an extended one.
+ * @callback extendStoreCallback
+ * @param {Vuex.Store} store
+ * @param {Element} el
+ */
+
+/**
* Initialize the IDE on the given element.
*
* @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE).
* @param {Component} options.rootComponent -
* Component that overrides the root component.
- * @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
+ * @param {extendStoreCallback} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
export function initIde(el, options = {}) {
@@ -53,16 +61,6 @@ export function initIde(el, options = {}) {
});
}
-// tell webpack to load assets from origin so that web workers don't break
-export function resetServiceWorkersPublicPath() {
- // __webpack_public_path__ is a global variable that can be used to adjust
- // the webpack publicPath setting at runtime.
- // see: https://webpack.js.org/guides/public-path/
- const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
- __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
-}
-
/**
* Start the IDE.
*
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 60bddb34977..a15f04075d9 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,12 +13,12 @@ export default class Model {
(this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw,
undefined,
- new Uri(false, false, `original/${this.path}`),
+ new Uri('gitlab', false, `original/${this.path}`),
)),
(this.model = monacoEditor.createModel(
this.content,
undefined,
- new Uri(false, false, this.path),
+ new Uri('gitlab', false, this.path),
)),
);
if (this.file.mrChange) {
@@ -26,7 +26,7 @@ export default class Model {
(this.baseModel = monacoEditor.createModel(
this.file.baseRaw,
undefined,
- new Uri(false, false, `target/${this.path}`),
+ new Uri('gitlab', false, `target/${this.path}`),
)),
);
}
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
new file mode 100644
index 00000000000..5dfba8fe531
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -0,0 +1,113 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
+import { decorateData, sortTree } from '../stores/utils';
+
+export const splitParent = path => {
+ const idx = path.lastIndexOf('/');
+
+ return {
+ parent: idx >= 0 ? path.substring(0, idx) : null,
+ name: idx >= 0 ? path.substring(idx + 1) : path,
+ };
+};
+
+/**
+ * Create file objects from a list of file paths.
+ */
+export const decorateFiles = ({
+ data,
+ projectId,
+ branchId,
+ tempFile = false,
+ content = '',
+ base64 = false,
+}) => {
+ const treeList = [];
+ const entries = {};
+
+ // These mutable variable references end up being exported and used by `createTempEntry`
+ let file;
+ let parentPath;
+
+ const insertParent = path => {
+ if (!path) {
+ return null;
+ } else if (entries[path]) {
+ return entries[path];
+ }
+
+ const { parent, name } = splitParent(path);
+ const parentFolder = parent && insertParent(parent);
+ parentPath = parentFolder && parentFolder.path;
+
+ const tree = decorateData({
+ projectId,
+ branchId,
+ id: path,
+ name,
+ path,
+ url: `/${projectId}/tree/${branchId}/-/${path}/`,
+ type: 'tree',
+ parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
+ tempFile,
+ changed: tempFile,
+ opened: tempFile,
+ parentPath,
+ });
+
+ Object.assign(entries, {
+ [path]: tree,
+ });
+
+ if (parentFolder) {
+ parentFolder.tree.push(tree);
+ } else {
+ treeList.push(tree);
+ }
+
+ return tree;
+ };
+
+ data.forEach(path => {
+ const { parent, name } = splitParent(path);
+
+ const fileFolder = parent && insertParent(parent);
+
+ if (name) {
+ parentPath = fileFolder && fileFolder.path;
+
+ file = decorateData({
+ projectId,
+ branchId,
+ id: path,
+ name,
+ path,
+ url: `/${projectId}/blob/${branchId}/-/${path}`,
+ type: 'blob',
+ parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
+ tempFile,
+ changed: tempFile,
+ content,
+ base64,
+ previewMode: viewerInformationForPath(name),
+ parentPath,
+ });
+
+ Object.assign(entries, {
+ [path]: file,
+ });
+
+ if (fileFolder) {
+ fileFolder.tree.push(file);
+ } else {
+ treeList.push(file);
+ }
+ }
+ });
+
+ return {
+ entries,
+ treeList: sortTree(treeList),
+ file,
+ parentPath,
+ };
+};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 13449592e62..ba33b6826d6 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -40,6 +40,9 @@ export default {
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
+ getProjectMergeRequests(projectId, params = {}) {
+ return Api.projectMergeRequests(projectId, params);
+ },
getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
return Api.projectMergeRequest(projectId, mergeRequestId, params);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index e10a132ab4b..7b660bda081 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types';
-import FilesDecoratorWorker from './workers/files_decorator_worker';
+import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
export const redirectToUrl = (_, url) => visitUrl(url);
@@ -56,7 +56,6 @@ export const createTempEntry = (
{ name, type, content = '', base64 = false },
) =>
new Promise(resolve => {
- const worker = new FilesDecoratorWorker();
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
@@ -74,31 +73,7 @@ export const createTempEntry = (
return null;
}
- worker.addEventListener('message', ({ data }) => {
- const { file, parentPath } = data;
-
- worker.terminate();
-
- commit(types.CREATE_TMP_ENTRY, {
- data,
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- });
-
- if (type === 'blob') {
- commit(types.TOGGLE_FILE_OPEN, file.path);
- commit(types.ADD_FILE_TO_CHANGED, file.path);
- dispatch('setFileActive', file.path);
- }
-
- if (parentPath && !state.entries[parentPath].opened) {
- commit(types.TOGGLE_TREE_OPEN, parentPath);
- }
-
- resolve(file);
- });
-
- worker.postMessage({
+ const data = decorateFiles({
data: [fullName],
projectId: state.currentProjectId,
branchId: state.currentBranchId,
@@ -107,6 +82,25 @@ export const createTempEntry = (
base64,
content,
});
+ const { file, parentPath } = data;
+
+ commit(types.CREATE_TMP_ENTRY, {
+ data,
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ });
+
+ if (type === 'blob') {
+ commit(types.TOGGLE_FILE_OPEN, file.path);
+ commit(types.ADD_FILE_TO_CHANGED, file.path);
+ dispatch('setFileActive', file.path);
+ }
+
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+
+ resolve(file);
return null;
});
@@ -215,15 +209,27 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
-export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
+export const renameEntry = (
+ { dispatch, commit, state },
+ { path, name, entryPath = null, parentPath },
+) => {
const entry = state.entries[entryPath || path];
- commit(types.RENAME_ENTRY, { path, name, entryPath });
+ commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath });
if (entry.type === 'tree') {
- state.entries[entryPath || path].tree.forEach(f =>
- dispatch('renameEntry', { path, name, entryPath: f.path }),
- );
+ const slashedParentPath = parentPath ? `${parentPath}/` : '';
+ const targetEntry = entryPath ? entryPath.split('/').pop() : name;
+ const newParentPath = `${slashedParentPath}${targetEntry}`;
+
+ state.entries[entryPath || path].tree.forEach(f => {
+ dispatch('renameEntry', {
+ path,
+ name,
+ entryPath: f.path,
+ parentPath: newParentPath,
+ });
+ });
}
if (!entryPath && !entry.tempFile) {
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 18c24369996..362ced248a1 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -4,6 +4,38 @@ import service from '../../services';
import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
+export const getMergeRequestsForBranch = ({ commit }, { projectId, branchId } = {}) =>
+ service
+ .getProjectMergeRequests(`${projectId}`, {
+ source_branch: branchId,
+ order_by: 'created_at',
+ per_page: 1,
+ })
+ .then(({ data }) => {
+ if (data.length > 0) {
+ const currentMR = data[0];
+
+ commit(types.SET_MERGE_REQUEST, {
+ projectPath: projectId,
+ mergeRequestId: currentMR.iid,
+ mergeRequest: currentMR,
+ });
+
+ commit(types.SET_CURRENT_MERGE_REQUEST, `${currentMR.iid}`);
+ }
+ })
+ .catch(e => {
+ flash(
+ __(`Error fetching merge requests for ${branchId}`),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ throw e;
+ });
+
export const getMergeRequestData = (
{ commit, dispatch, state },
{ projectId, mergeRequestId, targetProjectId = null, force = false } = {},
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index b65f631c99c..06ed5c0b572 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -136,17 +136,24 @@ export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath
return dispatch('getFiles', {
projectId,
branchId,
- }).then(() => {
- if (basePath) {
- const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
- const treeEntryKey = Object.keys(state.entries).find(
- key => key === path && !state.entries[key].pending,
- );
- const treeEntry = state.entries[treeEntryKey];
+ })
+ .then(() => {
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
+ const treeEntryKey = Object.keys(state.entries).find(
+ key => key === path && !state.entries[key].pending,
+ );
+ const treeEntry = state.entries[treeEntryKey];
- if (treeEntry) {
- dispatch('handleTreeEntryAction', treeEntry);
+ if (treeEntry) {
+ dispatch('handleTreeEntryAction', treeEntry);
+ }
}
- }
- });
+ })
+ .then(() => {
+ dispatch('getMergeRequestsForBranch', {
+ projectId,
+ branchId,
+ });
+ });
};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index de5f6050074..3d83e4a9ba5 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,7 +1,8 @@
+import _ from 'underscore';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
-import FilesDecoratorWorker from '../workers/files_decorator_worker';
+import { decorateFiles } from '../../lib/files';
export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
@@ -32,6 +33,19 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('showTreeEntry', row.path);
};
+export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeList }) => {
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+};
+
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (
@@ -45,31 +59,19 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
service
.getFiles(selectedProject.web_url, branchId)
.then(({ data }) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', e => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, {
- treePath: `${projectId}/${branchId}`,
- data: treeList,
- });
- commit(types.TOGGLE_LOADING, {
- entry: selectedTree,
- forceValue: false,
- });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
+ const { entries, treeList } = decorateFiles({
data,
projectId,
branchId,
});
+
+ commit(types.SET_ENTRIES, entries);
+
+ // Defer setting the directory data because this triggers some intense rendering.
+ // The entries is all we need to load the file editor.
+ _.defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
+
+ resolve();
})
.catch(e => {
if (e.response.status === 404) {
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index d97a950a8b2..24c2f71ae2b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -202,7 +202,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch(
'setErrorMessage',
{
- text: __('An error accured whilst committing your changes.'),
+ text: __('An error occurred whilst committing your changes.'),
action: () =>
dispatch('commitChanges').then(() =>
dispatch('setErrorMessage', null, { root: true }),
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 78cdfda74f0..9b9f4b21f1c 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -206,19 +206,17 @@ export default {
}
}
},
- [types.RENAME_ENTRY](state, { path, name, entryPath = null }) {
+ [types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) {
const oldEntry = state.entries[entryPath || path];
- const nameRegex =
- !entryPath && oldEntry.type === 'blob'
- ? new RegExp(`${oldEntry.name}$`)
- : new RegExp(`^${path}`);
- const newPath = oldEntry.path.replace(nameRegex, name);
- const parentPath = oldEntry.parentPath ? oldEntry.parentPath.replace(nameRegex, name) : '';
+ const slashedParentPath = parentPath ? `${parentPath}/` : '';
+ const newPath = entryPath
+ ? `${slashedParentPath}${oldEntry.name}`
+ : `${slashedParentPath}${name}`;
state.entries[newPath] = {
...oldEntry,
id: newPath,
- key: `${name}-${oldEntry.type}-${oldEntry.id}`,
+ key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
@@ -228,6 +226,7 @@ export default {
parentPath,
raw: '',
};
+
oldEntry.moved = true;
oldEntry.movedPath = newPath;
@@ -256,6 +255,7 @@ export default {
Vue.delete(state.entries, oldEntry.path);
}
},
+
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 0ede76fd1e0..0b2a18e9c8a 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -75,8 +75,7 @@ export const decorateData = entity => {
parentPath = '',
} = entity;
- return {
- ...dataStructure(),
+ return Object.assign(dataStructure(), {
id,
projectId,
branchId,
@@ -97,7 +96,7 @@ export const decorateData = entity => {
file_lock,
html,
parentPath,
- };
+ });
};
export const findEntry = (tree, type, name, prop = 'name') =>
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
deleted file mode 100644
index fa35c215880..00000000000
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateData, sortTree } from '../utils';
-
-// eslint-disable-next-line no-restricted-globals
-self.addEventListener('message', e => {
- const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
-
- const treeList = [];
- let file;
- let parentPath;
- const entries = data.reduce((acc, path) => {
- const pathSplit = path.split('/');
- const blobName = pathSplit.pop().trim();
-
- if (pathSplit.length > 0) {
- pathSplit.reduce((pathAcc, folderName) => {
- const parentFolder = acc[pathAcc[pathAcc.length - 1]];
- const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
- const foundEntry = acc[folderPath];
-
- if (!foundEntry) {
- parentPath = parentFolder ? parentFolder.path : null;
-
- const tree = decorateData({
- projectId,
- branchId,
- id: folderPath,
- name: folderName,
- path: folderPath,
- url: `/${projectId}/tree/${branchId}/-/${folderPath}/`,
- type: 'tree',
- parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
- tempFile,
- changed: tempFile,
- opened: tempFile,
- parentPath,
- });
-
- Object.assign(acc, {
- [folderPath]: tree,
- });
-
- if (parentFolder) {
- parentFolder.tree.push(tree);
- } else {
- treeList.push(tree);
- }
-
- pathAcc.push(tree.path);
- } else {
- pathAcc.push(foundEntry.path);
- }
-
- return pathAcc;
- }, []);
- }
-
- if (blobName !== '') {
- const fileFolder = acc[pathSplit.join('/')];
- parentPath = fileFolder ? fileFolder.path : null;
-
- file = decorateData({
- projectId,
- branchId,
- id: path,
- name: blobName,
- path,
- url: `/${projectId}/blob/${branchId}/-/${path}`,
- type: 'blob',
- parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
- tempFile,
- changed: tempFile,
- content,
- base64,
- previewMode: viewerInformationForPath(blobName),
- parentPath,
- });
-
- Object.assign(acc, {
- [path]: file,
- });
-
- if (fileFolder) {
- fileFolder.tree.push(file);
- } else {
- treeList.push(file);
- }
- }
-
- return acc;
- }, {});
-
- // eslint-disable-next-line no-restricted-globals
- self.postMessage({
- entries,
- treeList: sortTree(treeList),
- file,
- parentPath,
- });
-});
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
new file mode 100644
index 00000000000..777f8fa6691
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -0,0 +1,101 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { __, sprintf } from '~/locale';
+import ImportedProjectTableRow from './imported_project_table_row.vue';
+import ProviderRepoTableRow from './provider_repo_table_row.vue';
+import eventHub from '../event_hub';
+
+export default {
+ name: 'ImportProjectsTable',
+ components: {
+ ImportedProjectTableRow,
+ ProviderRepoTableRow,
+ LoadingButton,
+ GlLoadingIcon,
+ },
+ props: {
+ providerTitle: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']),
+ ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
+
+ emptyStateText() {
+ return sprintf(__('No %{providerTitle} repositories available to import'), {
+ providerTitle: this.providerTitle,
+ });
+ },
+
+ fromHeaderText() {
+ return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle });
+ },
+ },
+
+ mounted() {
+ return this.fetchRepos();
+ },
+
+ beforeDestroy() {
+ this.stopJobsPolling();
+ this.clearJobsEtagPoll();
+ },
+
+ methods: {
+ ...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']),
+
+ importAll() {
+ eventHub.$emit('importAll');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <p class="light text-nowrap mt-2 my-sm-0">
+ {{ s__('ImportProjects|Select the projects you want to import') }}
+ </p>
+ <loading-button
+ container-class="btn btn-success js-import-all"
+ :loading="isImportingAnyRepo"
+ :label="__('Import all repositories')"
+ :disabled="!hasProviderRepos"
+ type="button"
+ @click="importAll"
+ />
+ </div>
+ <gl-loading-icon
+ v-if="isLoadingRepos"
+ class="js-loading-button-icon import-projects-loading-icon"
+ :size="4"
+ />
+ <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
+ <table class="table import-table">
+ <thead>
+ <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
+ <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
+ <th class="import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <imported-project-table-row
+ v-for="project in importedProjects"
+ :key="project.id"
+ :project="project"
+ />
+ <provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" />
+ </tbody>
+ </table>
+ </div>
+ <div v-else class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/import_status.vue b/app/assets/javascripts/import_projects/components/import_status.vue
new file mode 100644
index 00000000000..9e3347a657f
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/import_status.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import STATUS_MAP from '../constants';
+
+export default {
+ name: 'ImportStatus',
+ components: {
+ CiIcon,
+ GlLoadingIcon,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ mappedStatus() {
+ return STATUS_MAP[this.status];
+ },
+
+ ciIconStatus() {
+ const { icon } = this.mappedStatus;
+
+ return {
+ icon: `status_${icon}`,
+ group: icon,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon
+ v-if="mappedStatus.loadingIcon"
+ :inline="true"
+ :class="mappedStatus.textClass"
+ class="align-middle mr-2"
+ />
+ <ci-icon v-else css-classes="align-middle mr-2" :status="ciIconStatus" />
+ <span :class="mappedStatus.textClass">{{ mappedStatus.text }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
new file mode 100644
index 00000000000..ab2bd87ee9f
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
@@ -0,0 +1,55 @@
+<script>
+import ImportStatus from './import_status.vue';
+import { STATUSES } from '../constants';
+
+export default {
+ name: 'ImportedProjectTableRow',
+ components: {
+ ImportStatus,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ displayFullPath() {
+ return this.project.fullPath.replace(/^\//, '');
+ },
+
+ isFinished() {
+ return this.project.importStatus === STATUSES.FINISHED;
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="js-imported-project import-row">
+ <td>
+ <a
+ :href="project.providerLink"
+ rel="noreferrer noopener"
+ target="_blank"
+ class="js-provider-link"
+ >
+ {{ project.importSource }}
+ </a>
+ </td>
+ <td class="js-full-path">{{ displayFullPath }}</td>
+ <td><import-status :status="project.importStatus" /></td>
+ <td>
+ <a
+ v-if="isFinished"
+ class="btn btn-default js-go-to-project"
+ :href="project.fullPath"
+ rel="noreferrer noopener"
+ target="_blank"
+ >
+ {{ __('Go to project') }}
+ </a>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
new file mode 100644
index 00000000000..7cc29fa1b91
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -0,0 +1,110 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import { __ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import eventHub from '../event_hub';
+import { STATUSES } from '../constants';
+import ImportStatus from './import_status.vue';
+
+export default {
+ name: 'ProviderRepoTableRow',
+ components: {
+ Select2Select,
+ LoadingButton,
+ ImportStatus,
+ },
+ props: {
+ repo: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ targetNamespace: this.$store.state.defaultTargetNamespace,
+ newName: this.repo.sanitizedName,
+ };
+ },
+
+ computed: {
+ ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']),
+
+ ...mapGetters(['namespaceSelectOptions']),
+
+ importButtonText() {
+ return this.ciCdOnly ? __('Connect') : __('Import');
+ },
+
+ select2Options() {
+ return {
+ data: this.namespaceSelectOptions,
+ containerCssClass:
+ 'import-namespace-select js-namespace-select qa-project-namespace-select',
+ };
+ },
+
+ isLoadingImport() {
+ return this.reposBeingImported.includes(this.repo.id);
+ },
+
+ status() {
+ return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE;
+ },
+ },
+
+ created() {
+ eventHub.$on('importAll', () => this.importRepo());
+ },
+
+ methods: {
+ ...mapActions(['fetchImport']),
+
+ importRepo() {
+ return this.fetchImport({
+ newName: this.newName,
+ targetNamespace: this.targetNamespace,
+ repo: this.repo,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="qa-project-import-row js-provider-repo import-row">
+ <td>
+ <a
+ :href="repo.providerLink"
+ rel="noreferrer noopener"
+ target="_blank"
+ class="js-provider-link"
+ >
+ {{ repo.fullName }}
+ </a>
+ </td>
+ <td class="d-flex flex-wrap flex-lg-nowrap">
+ <select2-select v-model="targetNamespace" :options="select2Options" />
+ <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
+ >/</span
+ >
+ <input
+ v-model="newName"
+ type="text"
+ class="form-control import-project-name-input js-new-name qa-project-path-field"
+ />
+ </td>
+ <td><import-status :status="status" /></td>
+ <td>
+ <button
+ v-if="!isLoadingImport"
+ type="button"
+ class="qa-import-button js-import-button btn btn-default"
+ @click="importRepo"
+ >
+ {{ importButtonText }}
+ </button>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_projects/constants.js b/app/assets/javascripts/import_projects/constants.js
new file mode 100644
index 00000000000..ad33ca158d2
--- /dev/null
+++ b/app/assets/javascripts/import_projects/constants.js
@@ -0,0 +1,48 @@
+import { __ } from '../locale';
+
+// The `scheduling` status is only present on the client-side,
+// it is used as the status when we are requesting to start an import.
+
+export const STATUSES = {
+ FINISHED: 'finished',
+ FAILED: 'failed',
+ SCHEDULED: 'scheduled',
+ STARTED: 'started',
+ NONE: 'none',
+ SCHEDULING: 'scheduling',
+};
+
+const STATUS_MAP = {
+ [STATUSES.FINISHED]: {
+ icon: 'success',
+ text: __('Done'),
+ textClass: 'text-success',
+ },
+ [STATUSES.FAILED]: {
+ icon: 'failed',
+ text: __('Failed'),
+ textClass: 'text-danger',
+ },
+ [STATUSES.SCHEDULED]: {
+ icon: 'pending',
+ text: __('Scheduled'),
+ textClass: 'text-warning',
+ },
+ [STATUSES.STARTED]: {
+ icon: 'running',
+ text: __('Running…'),
+ textClass: 'text-info',
+ },
+ [STATUSES.NONE]: {
+ icon: 'created',
+ text: __('Not started'),
+ textClass: 'text-muted',
+ },
+ [STATUSES.SCHEDULING]: {
+ loadingIcon: true,
+ text: __('Scheduling'),
+ textClass: 'text-warning',
+ },
+};
+
+export default STATUS_MAP;
diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/monitoring/event_hub.js
+++ b/app/assets/javascripts/import_projects/event_hub.js
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
new file mode 100644
index 00000000000..2d99d716609
--- /dev/null
+++ b/app/assets/javascripts/import_projects/index.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { mapActions } from 'vuex';
+import Translate from '../vue_shared/translate';
+import ImportProjectsTable from './components/import_projects_table.vue';
+import { parseBoolean } from '../lib/utils/common_utils';
+import createStore from './store';
+
+Vue.use(Translate);
+
+export default function mountImportProjectsTable(mountElement) {
+ if (!mountElement) return undefined;
+
+ const {
+ reposPath,
+ provider,
+ providerTitle,
+ canSelectNamespace,
+ jobsPath,
+ importPath,
+ ciCdOnly,
+ } = mountElement.dataset;
+
+ const store = createStore();
+ return new Vue({
+ el: mountElement,
+ store,
+
+ created() {
+ this.setInitialData({
+ reposPath,
+ provider,
+ jobsPath,
+ importPath,
+ defaultTargetNamespace: gon.current_username,
+ ciCdOnly: parseBoolean(ciCdOnly),
+ canSelectNamespace: parseBoolean(canSelectNamespace),
+ });
+ },
+
+ methods: {
+ ...mapActions(['setInitialData']),
+ },
+
+ render(createElement) {
+ return createElement(ImportProjectsTable, { props: { providerTitle } });
+ },
+ });
+}
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
new file mode 100644
index 00000000000..c44500937cc
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -0,0 +1,106 @@
+import Visibility from 'visibilityjs';
+import * as types from './mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import Poll from '~/lib/utils/poll';
+import createFlash from '~/flash';
+import { s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+let eTagPoll;
+
+export const clearJobsEtagPoll = () => {
+ eTagPoll = null;
+};
+export const stopJobsPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+export const restartJobsPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+
+export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos);
+export const receiveReposSuccess = ({ commit }, repos) =>
+ commit(types.RECEIVE_REPOS_SUCCESS, repos);
+export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR);
+export const fetchRepos = ({ state, dispatch }) => {
+ dispatch('requestRepos');
+
+ return axios
+ .get(state.reposPath)
+ .then(({ data }) =>
+ dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ )
+ .then(() => dispatch('fetchJobs'))
+ .catch(() => {
+ createFlash(
+ sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
+ provider: state.provider,
+ }),
+ );
+
+ dispatch('receiveReposError');
+ });
+};
+
+export const requestImport = ({ commit, state }, repoId) => {
+ if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId);
+};
+export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) =>
+ commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId });
+export const receiveImportError = ({ commit }, repoId) =>
+ commit(types.RECEIVE_IMPORT_ERROR, repoId);
+export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => {
+ dispatch('requestImport', repo.id);
+
+ return axios
+ .post(state.importPath, {
+ ci_cd_only: state.ciCdOnly,
+ new_name: newName,
+ repo_id: repo.id,
+ target_namespace: targetNamespace,
+ })
+ .then(({ data }) =>
+ dispatch('receiveImportSuccess', {
+ importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
+ repoId: repo.id,
+ }),
+ )
+ .catch(() => {
+ createFlash(s__('ImportProjects|Importing the project failed'));
+
+ dispatch('receiveImportError', { repoId: repo.id });
+ });
+};
+
+export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
+ commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
+export const fetchJobs = ({ state, dispatch }) => {
+ if (eTagPoll) return;
+
+ eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(state.jobsPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data }) =>
+ dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
+ errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ dispatch('restartJobsPolling');
+ } else {
+ dispatch('stopJobsPolling');
+ }
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
new file mode 100644
index 00000000000..f03474a8404
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -0,0 +1,20 @@
+export const namespaceSelectOptions = state => {
+ const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
+ id: fullPath,
+ text: fullPath,
+ }));
+
+ return [
+ { text: 'Groups', children: serializedNamespaces },
+ {
+ text: 'Users',
+ children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
+ },
+ ];
+};
+
+export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
+
+export const hasProviderRepos = state => state.providerRepos.length > 0;
+
+export const hasImportedProjects = state => state.importedProjects.length > 0;
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
new file mode 100644
index 00000000000..f666e2ebf33
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+ });
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
new file mode 100644
index 00000000000..6ba3fd6f29e
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -0,0 +1,11 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+
+export const REQUEST_REPOS = 'REQUEST_REPOS';
+export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
+export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
+
+export const REQUEST_IMPORT = 'REQUEST_IMPORT';
+export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
+export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
+
+export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
new file mode 100644
index 00000000000..b88de0268e7
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+
+ [types.REQUEST_REPOS](state) {
+ state.isLoadingRepos = true;
+ },
+
+ [types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) {
+ state.isLoadingRepos = false;
+
+ state.importedProjects = importedProjects;
+ state.providerRepos = providerRepos;
+ state.namespaces = namespaces;
+ },
+
+ [types.RECEIVE_REPOS_ERROR](state) {
+ state.isLoadingRepos = false;
+ },
+
+ [types.REQUEST_IMPORT](state, repoId) {
+ state.reposBeingImported.push(repoId);
+ },
+
+ [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
+ const existingRepoIndex = state.reposBeingImported.indexOf(repoId);
+ if (state.reposBeingImported.includes(repoId))
+ state.reposBeingImported.splice(existingRepoIndex, 1);
+
+ const providerRepoIndex = state.providerRepos.findIndex(
+ providerRepo => providerRepo.id === repoId,
+ );
+ state.providerRepos.splice(providerRepoIndex, 1);
+ state.importedProjects.unshift(importedProject);
+ },
+
+ [types.RECEIVE_IMPORT_ERROR](state, repoId) {
+ const repoIndex = state.reposBeingImported.indexOf(repoId);
+ if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1);
+ },
+
+ [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
+ updatedProjects.forEach(updatedProject => {
+ const existingProject = state.importedProjects.find(
+ importedProject => importedProject.id === updatedProject.id,
+ );
+
+ Vue.set(existingProject, 'importStatus', updatedProject.importStatus);
+ });
+ },
+};
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
new file mode 100644
index 00000000000..637fef6e53c
--- /dev/null
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ reposPath: '',
+ importPath: '',
+ jobsPath: '',
+ currentProjectId: '',
+ provider: '',
+ currentUsername: '',
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ reposBeingImported: [],
+ isLoadingRepos: false,
+ canSelectNamespace: false,
+ ciCdOnly: false,
+});
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index 612c524ca1c..e0fb58ef195 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -11,10 +11,14 @@ class AutoWidthDropdownSelect {
init() {
const { dropdownClass } = this;
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
return this;
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index f3d722409b0..48e7ed1318d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -7,10 +7,14 @@ export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index c81a2230310..9336b71cfd7 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -44,6 +44,7 @@ export default class IssuableForm {
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
@@ -120,35 +121,39 @@ export default class IssuableForm {
}
initTargetBranchDropdown() {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js
index 2c80cf1797a..40916c9d27f 100644
--- a/app/assets/javascripts/issuable_suggestions/index.js
+++ b/app/assets/javascripts/issuable_suggestions/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import defaultClient from '~/lib/graphql';
+import createDefaultClient from '~/lib/graphql';
import App from './components/app.vue';
Vue.use(VueApollo);
@@ -10,7 +10,7 @@ export default function() {
const issueTitle = document.getElementById('issue_title');
const { projectPath } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient,
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e4e2eab2acd..bd757a76ee7 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,7 @@
<script>
import Visibility from 'visibilityjs';
+import { __, s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
@@ -106,11 +108,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
projectPath: {
type: String,
required: true,
@@ -129,6 +126,11 @@ export default {
required: false,
default: true,
},
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
const store = new Store({
@@ -140,6 +142,7 @@ export default {
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
+ lock_version: this.lockVersion,
});
return {
@@ -160,6 +163,9 @@ export default {
const titleChanged = this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
+ defaultErrorMessage() {
+ return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
+ },
},
created() {
this.service = new Service(this.endpoint);
@@ -201,11 +207,22 @@ export default {
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
- if (this.showForm && this.issueChanged) {
- event.returnValue = 'Are you sure you want to lose your issue information?';
+ if (this.showForm && this.issueChanged && !this.showRecaptcha) {
+ event.returnValue = __('Are you sure you want to lose your issue information?');
}
return undefined;
},
+ updateStoreState() {
+ return this.service
+ .getData()
+ .then(res => res.data)
+ .then(data => {
+ this.store.updateState(data);
+ })
+ .catch(() => {
+ createFlash(this.defaultErrorMessage);
+ });
+ },
openForm() {
if (!this.showForm) {
@@ -213,6 +230,7 @@ export default {
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
+ lock_version: this.state.lock_version,
lockedWarningVisible: false,
updateLoading: false,
});
@@ -231,20 +249,24 @@ export default {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
-
- return this.service.getData();
})
- .then(res => res.data)
- .then(data => {
- this.store.updateState(data);
+ .then(this.updateStoreState)
+ .then(() => {
eventHub.$emit('close.form');
})
- .catch(error => {
- if (error && error.name === 'SpamError') {
+ .catch((error = {}) => {
+ const { name, response = {} } = error;
+
+ if (name === 'SpamError') {
this.openRecaptcha();
} else {
- eventHub.$emit('close.form');
- window.Flash(`Error updating ${this.issuableType}`);
+ let errMsg = this.defaultErrorMessage;
+
+ if (response.data && response.data.errors) {
+ errMsg += `. ${response.data.errors.join(' ')}`;
+ }
+
+ createFlash(errMsg);
}
});
},
@@ -268,8 +290,9 @@ export default {
visitUrl(data.web_url);
})
.catch(() => {
- eventHub.$emit('close.form');
- window.Flash(`Error deleting ${this.issuableType}`);
+ createFlash(
+ sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
+ );
});
},
},
@@ -285,7 +308,6 @@ export default {
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
- :markdown-version="markdownVersion"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
@@ -313,6 +335,8 @@ export default {
:task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
+ :lock-version="state.lock_version"
+ @taskListUpdateFailed="updateStoreState"
/>
<edited-component
v-if="hasUpdated"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 5ca88d75063..58f14bac8c8 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,5 +1,7 @@
<script>
import $ from 'jquery';
+import { s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
@@ -35,6 +37,11 @@ export default {
required: false,
default: null,
},
+ lockVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -67,8 +74,10 @@ export default {
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
+ lockVersion: this.lockVersion,
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
+ onError: this.taskListUpdateError.bind(this),
});
}
},
@@ -82,6 +91,21 @@ export default {
}
},
+ taskListUpdateError() {
+ createFlash(
+ sprintf(
+ s__(
+ 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
+ ),
+ {
+ issueType: this.issuableType,
+ },
+ ),
+ );
+
+ this.$emit('taskListUpdateFailed');
+ },
+
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 90258c0e044..299130e56ae 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -20,11 +20,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
canAttachFile: {
type: Boolean,
required: false,
@@ -48,7 +43,6 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 45b60bc3392..eade31f1d14 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -39,11 +39,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
projectPath: {
type: String,
required: true,
@@ -101,7 +96,6 @@ export default {
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 32044d6da25..3c17e73ccec 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -1,3 +1,5 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
export default class Store {
constructor(initialState) {
this.state = initialState;
@@ -6,6 +8,7 @@ export default class Store {
description: '',
lockedWarningVisible: false,
updateLoading: false,
+ lock_version: 0,
};
}
@@ -14,14 +17,10 @@ export default class Store {
this.formState.lockedWarningVisible = true;
}
+ Object.assign(this.state, convertObjectPropsToCamelCase(data));
this.state.titleHtml = data.title;
- this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description;
- this.state.descriptionText = data.description_text;
- this.state.taskStatus = data.task_status;
- this.state.updatedAt = data.updated_at;
- this.state.updatedByName = data.updated_by_name;
- this.state.updatedByPath = data.updated_by_path;
+ this.state.lock_version = data.lock_version;
}
stateShouldUpdate(data) {
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index e0f55518eef..7076a79dd5d 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -45,7 +45,7 @@ export default {
/>
<span v-if="mergeRequest">
- {{ __('in') }}
+ in
<gl-link :href="mergeRequest.path" class="js-link-commit link-commit"
>!{{ mergeRequest.iid }}</gl-link
>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 786ab16992d..dbadd224251 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -34,6 +34,7 @@ export default {
StuckBlock,
Sidebar,
GlLoadingIcon,
+ SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
},
mixins: [delayedJobMixin],
props: {
@@ -80,11 +81,11 @@ export default {
'hasError',
]),
...mapGetters([
- 'headerActions',
'headerTime',
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
+ 'shouldRenderSharedRunnerLimitWarning',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
@@ -202,7 +203,6 @@ export default {
:item-id="job.id"
:time="headerTime"
:user="job.user"
- :actions="headerActions"
:has-sidebar-button="true"
:should-render-triggered-label="shouldRenderTriggeredLabel"
:item-name="__('Job')"
@@ -223,6 +223,14 @@ export default {
:runners-path="runnerSettingsUrl"
/>
+ <shared-runner
+ v-if="shouldRenderSharedRunnerLimitWarning"
+ class="js-shared-runner-limit"
+ :quota-used="job.runners.quota.used"
+ :quota-limit="job.runners.quota.limit"
+ :runners-path="runnerHelpUrl"
+ />
+
<environments-block
v-if="hasEnvironment"
class="js-job-environment"
@@ -240,14 +248,19 @@ export default {
<div
v-if="job.archived"
ref="sticky"
- class="js-archived-job prepend-top-default archived-sticky sticky-top"
+ class="js-archived-job prepend-top-default archived-job"
+ :class="{ 'sticky-top border-bottom-0': hasTrace }"
>
<icon name="lock" class="align-text-bottom" />
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
- <div v-if="hasTrace" class="build-trace-container">
+ <div
+ v-if="hasTrace"
+ class="build-trace-container"
+ :class="{ 'prepend-top-default': !job.archived }"
+ >
<log-top-bar
:class="{
'sidebar-expanded': isSidebarOpen,
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index ad3e7dabc79..1691ac62100 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -48,8 +48,7 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
- let className =
- 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ let className = 'js-retry-button btn btn-retry';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
@@ -110,24 +109,27 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <div class="block d-flex align-items-center">
- <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4>
- <gl-link
- v-if="job.retry_path"
- :class="retryButtonClass"
- :href="job.retry_path"
- data-method="post"
- rel="nofollow"
- >{{ __('Retry') }}</gl-link
- >
- <gl-link
- v-if="job.terminal_path"
- :href="job.terminal_path"
- class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block"
- target="_blank"
- >
- {{ __('Debug') }} <icon name="external-link" />
- </gl-link>
+ <div class="block d-flex flex-nowrap align-items-center">
+ <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4>
+ <div class="flex-grow-1 flex-shrink-0 text-right">
+ <gl-link
+ v-if="job.retry_path"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >{{ __('Retry') }}</gl-link
+ >
+ <gl-link
+ v-if="job.cancel_path"
+ :href="job.cancel_path"
+ class="js-cancel-job btn btn-default"
+ data-method="post"
+ rel="nofollow"
+ >{{ __('Cancel') }}</gl-link
+ >
+ </div>
+
<gl-button
:aria-label="__('Toggle Sidebar')"
type="button"
@@ -137,22 +139,24 @@ export default {
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
</gl-button>
</div>
- <div v-if="job.retry_path || job.new_issue_path" class="block retry-link">
+
+ <div v-if="job.terminal_path || job.new_issue_path" class="block retry-link">
<gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
- class="js-new-issue btn btn-success btn-inverted"
+ class="js-new-issue btn btn-success btn-inverted float-left mr-2"
>{{ __('New issue') }}</gl-link
>
<gl-link
- v-if="job.retry_path"
- :href="job.retry_path"
- class="js-retry-job btn btn-inverted-secondary"
- data-method="post"
- rel="nofollow"
- >{{ __('Retry') }}</gl-link
+ v-if="job.terminal_path"
+ :href="job.terminal_path"
+ class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
+ target="_blank"
>
+ {{ __('Debug') }} <icon name="external-link" :size="14" />
+ </gl-link>
</div>
+
<div :class="{ block: renderBlock }">
<detail-row
v-if="job.duration"
@@ -193,16 +197,6 @@ export default {
tag
}}</span>
</p>
-
- <div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group">
- <gl-link
- :href="job.cancel_path"
- class="js-cancel-job btn btn-sm btn-default"
- data-method="post"
- rel="nofollow"
- >{{ __('Cancel') }}</gl-link
- >
- </div>
</div>
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" />
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 7f79e92067f..c5076d65ff9 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -39,7 +39,9 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ __('Pipeline') }}</span>
- <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a>
+ <a :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
+ >#{{ pipeline.id }}</a
+ >
<template v-if="hasRef">
{{ __('from') }}
<a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a>
@@ -55,7 +57,7 @@ export default {
<ul class="dropdown-menu">
<li v-for="stage in stages" :key="stage.name">
- <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage);">
+ <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)">
{{ stage.name }}
</button>
</li>
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 35e92b0b5d9..73c1cbc3a99 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -1,22 +1,6 @@
import _ from 'underscore';
-import { __ } from '~/locale';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-export const headerActions = state => {
- if (state.job.new_issue_path) {
- return [
- {
- label: __('New issue'),
- path: state.job.new_issue_path,
- cssClass:
- 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block',
- type: 'link',
- },
- ];
- }
- return [];
-};
-
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const shouldRenderCalloutMessage = state =>
@@ -44,6 +28,17 @@ export const emptyStateIllustration = state =>
export const emptyStateAction = state =>
(state.job && state.job.status && state.job.status.action) || null;
+/**
+ * Shared runners limit is only rendered when
+ * used quota is bigger or equal than the limit
+ *
+ * @returns {Boolean}
+ */
+export const shouldRenderSharedRunnerLimitWarning = state =>
+ !_.isEmpty(state.job.runners) &&
+ !_.isEmpty(state.job.runners.quota) &&
+ state.job.runners.quota.used >= state.job.runners.quota.limit;
+
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = state =>
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 062501d1d04..f134a54dd53 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -70,7 +70,18 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- $detachedLabel.appendTo($target);
+
+ const $labelEls = $target.find('li.label-list-item');
+
+ /*
+ * If there is a label element in the target, we'd want to
+ * append the new label just right next to it.
+ */
+ if ($labelEls.length) {
+ $labelEls.last().after($detachedLabel);
+ } else {
+ $detachedLabel.appendTo($target);
+ }
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index f7a611fbca0..cca4927c115 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -199,8 +199,8 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
- var $a,
- $li,
+ var linkEl,
+ listItemEl,
color,
colorEl,
indeterminate,
@@ -209,12 +209,11 @@ export default class LabelsSelect {
spacing,
i,
marked,
- dropdownName,
dropdownValue;
- $li = $('<li>');
- $a = $('<a href="#">');
+
selectedClass = [];
removesAll = label.id <= 0 || label.id == null;
+
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || [];
@@ -233,7 +232,6 @@ export default class LabelsSelect {
}
} else {
if (this.id(label)) {
- dropdownName = $dropdown.data('fieldName');
dropdownValue = this.id(label)
.toString()
.replace(/'/g, "\\'");
@@ -241,7 +239,7 @@ export default class LabelsSelect {
if (
$form.find(
"input[type='hidden'][name='" +
- dropdownName +
+ this.fieldName +
"'][value='" +
dropdownValue +
"']",
@@ -251,24 +249,34 @@ export default class LabelsSelect {
}
}
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ if (this.multiSelect && removesAll) {
selectedClass.push('dropdown-clear-active');
}
}
+
if (label.color) {
colorEl =
"<span class='dropdown-label-box' style='background: " + label.color + "'></span>";
} else {
colorEl = '';
}
+
+ linkEl = document.createElement('a');
+ linkEl.href = '#';
+
// We need to identify which items are actually labels
if (label.id) {
selectedClass.push('label-item');
- $a.attr('data-label-id', label.id);
+ linkEl.dataset.labelId = label.id;
}
- $a.addClass(selectedClass.join(' ')).html(`${colorEl} ${_.escape(label.title)}`);
- // Return generated html
- return $li.html($a).prop('outerHTML');
+
+ linkEl.className = selectedClass.join(' ');
+ linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
+
+ listItemEl = document.createElement('li');
+ listItemEl.appendChild(linkEl);
+
+ return listItemEl;
},
search: {
fields: ['title'],
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index b8c3c237eb3..4314e5e1afb 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -11,48 +11,53 @@ function hideEndFade($scrollingTabs) {
});
}
+function initDeferred() {
+ $(document).trigger('init.scrolling-tabs');
+}
+
export default function initLayoutNav() {
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();
initFlyOutNav();
- $(document)
- .on('init.scrolling-tabs', () => {
- const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
- $scrollingTabs.addClass('is-initialized');
+ // We need to init it on DomContentLoaded as others could also call it
+ $(document).on('init.scrolling-tabs', () => {
+ const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
+ $scrollingTabs.addClass('is-initialized');
- $(window)
- .on('resize.nav', () => {
- hideEndFade($scrollingTabs);
- })
- .trigger('resize.nav');
+ $(window)
+ .on('resize.nav', () => {
+ hideEndFade($scrollingTabs);
+ })
+ .trigger('resize.nav');
- $scrollingTabs.on('scroll', function tabsScrollEvent() {
- const $this = $(this);
- const currentPosition = $this.scrollLeft();
- const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+ $scrollingTabs.on('scroll', function tabsScrollEvent() {
+ const $this = $(this);
+ const currentPosition = $this.scrollLeft();
+ const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
- $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
- $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
- });
+ $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ });
- $scrollingTabs.each(function scrollTabsEachLoop() {
- const $this = $(this);
- const scrollingTabWidth = $this.width();
- const $active = $this.find('.active');
- const activeWidth = $active.width();
+ $scrollingTabs.each(function scrollTabsEachLoop() {
+ const $this = $(this);
+ const scrollingTabWidth = $this.width();
+ const $active = $this.find('.active');
+ const activeWidth = $active.width();
- if ($active.length) {
- const offset = $active.offset().left + activeWidth;
+ if ($active.length) {
+ const offset = $active.offset().left + activeWidth;
- if (offset > scrollingTabWidth - 30) {
- const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2;
+ if (offset > scrollingTabWidth - 30) {
+ const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2;
- $this.scrollLeft(scrollLeft);
- }
+ $this.scrollLeft(scrollLeft);
}
- });
- })
- .trigger('init.scrolling-tabs');
+ }
+ });
+ });
+
+ requestIdleCallback(initDeferred);
}
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index ee01a73a6e8..66f25b622e0 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -163,6 +163,7 @@ export default class LazyLoader {
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
+ img.classList.add('qa-js-lazy-loaded');
}
}
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 20a0f142d9e..64e4e899f44 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,9 +1,11 @@
import ApolloClient from 'apollo-boost';
import csrf from '~/lib/utils/csrf';
-export default new ApolloClient({
- uri: `${gon.relative_url_root}/api/graphql`,
- headers: {
- [csrf.headerKey]: csrf.token,
- },
-});
+export default (clientState = {}) =>
+ new ApolloClient({
+ uri: `${gon.relative_url_root}/api/graphql`,
+ headers: {
+ [csrf.headerKey]: csrf.token,
+ },
+ clientState,
+ });
diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js
new file mode 100644
index 00000000000..0f78756aac8
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/chart_utils.js
@@ -0,0 +1,83 @@
+const commonTooltips = () => ({
+ mode: 'x',
+ intersect: false,
+});
+
+const adjustedFontScale = () => ({
+ fontSize: 8,
+});
+
+const yAxesConfig = (shouldAdjustFontSize = false) => ({
+ yAxes: [
+ {
+ ticks: {
+ beginAtZero: true,
+ ...(shouldAdjustFontSize ? adjustedFontScale() : {}),
+ },
+ },
+ ],
+});
+
+const xAxesConfig = (shouldAdjustFontSize = false) => ({
+ xAxes: [
+ {
+ ticks: {
+ ...(shouldAdjustFontSize ? adjustedFontScale() : {}),
+ },
+ },
+ ],
+});
+
+const commonChartOptions = () => ({
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: false,
+});
+
+export const barChartOptions = shouldAdjustFontSize => ({
+ ...commonChartOptions(),
+ scales: {
+ ...yAxesConfig(shouldAdjustFontSize),
+ ...xAxesConfig(shouldAdjustFontSize),
+ },
+ tooltips: {
+ ...commonTooltips(),
+ displayColors: false,
+ callbacks: {
+ title() {
+ return '';
+ },
+ label({ xLabel, yLabel }) {
+ return `${xLabel}: ${yLabel}`;
+ },
+ },
+ },
+});
+
+export const pieChartOptions = commonChartOptions;
+
+export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize }) => ({
+ ...commonChartOptions(),
+ scales: {
+ ...yAxesConfig(shouldAdjustFontSize),
+ ...xAxesConfig(shouldAdjustFontSize),
+ },
+ elements: {
+ point: {
+ hitRadius: width / (numberOfPoints * 2),
+ },
+ },
+ tooltips: {
+ ...commonTooltips(),
+ caretSize: 0,
+ multiKeyBackground: 'rgba(0,0,0,0)',
+ callbacks: {
+ labelColor({ datasetIndex }, { config }) {
+ return {
+ backgroundColor: config.data.datasets[datasetIndex].backgroundColor,
+ borderColor: 'rgba(0,0,0,0)',
+ };
+ },
+ },
+ },
+});
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9e22cdc04e9..1af6b63efc9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,7 @@
+/**
+ * @module common-utils
+ */
+
import $ from 'jquery';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
@@ -118,14 +122,15 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
-export const isInViewport = el => {
+export const isInViewport = (el, offset = {}) => {
const rect = el.getBoundingClientRect();
+ const { top, left } = offset;
return (
- rect.top >= 0 &&
- rect.left >= 0 &&
+ rect.top >= (top || 0) &&
+ rect.left >= (left || 0) &&
rect.bottom <= window.innerHeight &&
- rect.right <= window.innerWidth
+ parseInt(rect.right, 10) <= window.innerWidth
);
};
@@ -216,6 +221,22 @@ export const scrollToElement = element => {
};
/**
+ * Returns a function that can only be invoked once between
+ * each browser screen repaint.
+ * @param {Function} fn
+ */
+export const debounceByAnimationFrame = fn => {
+ let requestId;
+
+ return function debounced(...args) {
+ if (requestId) {
+ window.cancelAnimationFrame(requestId);
+ }
+ requestId = window.requestAnimationFrame(() => fn.apply(this, args));
+ };
+};
+
+/**
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
@@ -425,34 +446,26 @@ export const historyPushState = newUrl => {
};
/**
- * Returns true for a String "true" and false otherwise.
- * This is the opposite of Boolean(...).toString()
+ * Returns true for a String value of "true" and false otherwise.
+ * This is the opposite of Boolean(...).toString().
+ * `parseBoolean` is idempotent.
*
* @param {String} value
* @returns {Boolean}
*/
-export const parseBoolean = value => value === 'true';
+export const parseBoolean = value => (value && value.toString()) === 'true';
/**
- * Converts permission provided as strings to booleans.
- *
- * @param {String} string
- * @returns {Boolean}
+ * @callback backOffCallback
+ * @param {Function} next
+ * @param {Function} stop
*/
-export const convertPermissionToBoolean = permission => {
- if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line no-console
- console.warn('convertPermissionToBoolean is deprecated! Please use parseBoolean instead.');
- }
-
- return parseBoolean(permission);
-};
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
- * @param {Function<next, stop>} fn function to be called
+ * @param {backOffCallback} fn function to be called
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
@@ -602,10 +615,18 @@ export const spriteIcon = (icon, className = '') => {
/**
* This method takes in object with snake_case property names
- * and returns new object with camelCase property names
+ * and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
+ *
+ * This method also supports additional params in `options` object
+ *
+ * @param {Object} obj - Object to be converted.
+ * @param {Object} options - Object containing additional options.
+ * @param {boolean} options.deep - FLag to allow deep object converting
+ * @param {Array[]} dropKeys - List of properties to discard while building new object
+ * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
@@ -613,12 +634,26 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}
const initial = Array.isArray(obj) ? [] : {};
+ const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
- if (options.deep && (isObject(val) || Array.isArray(val))) {
+ // Drop properties from new object if
+ // there are any mentioned in options
+ if (dropKeys.indexOf(prop) > -1) {
+ return acc;
+ }
+
+ // Skip converting properties in new object
+ // if there are any mentioned in options
+ if (ignoreKeyNames.indexOf(prop) > -1) {
+ result[prop] = obj[prop];
+ return acc;
+ }
+
+ if (deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
} else {
result[convertToCamelCase(prop)] = obj[prop];
@@ -673,6 +708,14 @@ export const NavigationType = {
TYPE_RESERVED: 255,
};
+/**
+ * Returns the value of `gon.ee`
+ * Used to check if it's the EE codebase or the CE one.
+ *
+ * @returns Boolean
+ */
+export const isEE = () => window.gon && window.gon.ee;
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 59007d5950e..d3fe8f77bd4 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -8,6 +8,14 @@ import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
/**
+ * This method allows you to create new Date instance from existing
+ * date instance without keeping the reference.
+ *
+ * @param {Date} date
+ */
+export const newDate = date => (date instanceof Date ? new Date(date.getTime()) : new Date());
+
+/**
* Returns i18n month names array.
* If `abbreviated` is provided, returns abbreviated
* name.
@@ -79,44 +87,67 @@ let timeagoInstance;
*/
export const getTimeago = () => {
if (!timeagoInstance) {
- const localeRemaining = (number, index) =>
- [
- [s__('Timeago|just now'), s__('Timeago|right now')],
- [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
- [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
- [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
- [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
- [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
- [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
- [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
- [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
- [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
- ][index];
-
- const locale = (number, index) =>
- [
- [s__('Timeago|just now'), s__('Timeago|right now')],
- [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
- [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
- [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
- [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
- [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
- [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
- [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
- [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
- [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
- ][index];
-
- timeago.register(timeagoLanguageCode, locale);
- timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
+ const memoizedLocaleRemaining = () => {
+ const cache = [];
+
+ const timeAgoLocaleRemaining = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
+ ];
+
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index]();
+ return cache[index];
+ };
+ };
+
+ const memoizedLocale = () => {
+ const cache = [];
+
+ const timeAgoLocale = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
+ ];
+
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocale[index] && timeAgoLocale[index]();
+ return cache[index];
+ };
+ };
+
+ timeago.register(timeagoLanguageCode, memoizedLocale());
+ timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
+
timeagoInstance = timeago();
}
@@ -124,35 +155,28 @@ export const getTimeago = () => {
};
/**
- * For the given element, renders a timeago instance.
- * @param {jQuery} $els
- */
-export const renderTimeago = $els => {
- const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
-
- // timeago.js sets timeouts internally for each timeago value to be updated in real time
- getTimeago().render(timeagoEls, timeagoLanguageCode);
-};
-
-/**
* For the given elements, sets a tooltip with a formatted date.
- * @param {jQuery}
+ * @param {JQuery} $timeagoEls
* @param {Boolean} setTimeago
*/
export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- $timeagoEls.each((i, el) => {
- if (setTimeago) {
+ getTimeago().render($timeagoEls, timeagoLanguageCode);
+
+ if (!setTimeago) {
+ return;
+ }
+
+ function addTimeAgoTooltip() {
+ $timeagoEls.each((i, el) => {
// Recreate with custom template
$(el).tooltip({
template:
'<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
- }
-
- el.classList.add('js-timeago-render');
- });
+ });
+ }
- renderTimeago($timeagoEls);
+ requestIdleCallback(addTimeAgoTooltip);
};
/**
@@ -321,23 +345,35 @@ export const getSundays = date => {
/**
* Returns list of Dates representing a timeframe of months from startDate and length
+ * This method also supports going back in time when `length` is negative number
*
- * @param {Date} startDate
+ * @param {Date} initialStartDate
* @param {Number} length
*/
-export const getTimeframeWindowFrom = (startDate, length) => {
- if (!(startDate instanceof Date) || !length) {
+export const getTimeframeWindowFrom = (initialStartDate, length) => {
+ if (!(initialStartDate instanceof Date) || !length) {
return [];
}
+ const startDate = newDate(initialStartDate);
+ const moveMonthBy = length > 0 ? 1 : -1;
+
+ startDate.setDate(1);
+ startDate.setHours(0, 0, 0, 0);
+
// Iterate and set date for the size of length
// and push date reference to timeframe list
- const timeframe = new Array(length)
- .fill()
- .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
+ const timeframe = new Array(Math.abs(length)).fill().map(() => {
+ const currentMonth = startDate.getTime();
+ startDate.setMonth(startDate.getMonth() + moveMonthBy);
+ return new Date(currentMonth);
+ });
// Change date of last timeframe item to last date of the month
- timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
+ // when length is positive
+ if (length > 0) {
+ timeframe[timeframe.length - 1].setDate(totalDaysInMonth(timeframe[timeframe.length - 1]));
+ }
return timeframe;
};
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index b41ffb44971..82ee83e4348 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -1,6 +1,9 @@
export default (buttonSelector, fileSelector) => {
const btn = document.querySelector(buttonSelector);
const fileInput = document.querySelector(fileSelector);
+
+ if (!btn || !fileInput) return;
+
const form = btn.closest('form');
btn.addEventListener('click', () => {
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
new file mode 100644
index 00000000000..18f9e2ed846
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -0,0 +1,40 @@
+import { sprintf, s__ } from '~/locale';
+
+/**
+ * Combines each given item into a noun series sentence fragment. It does this
+ * in a way that supports i18n by giving context and punctuation to the locale
+ * functions.
+ *
+ * **Examples:**
+ *
+ * - `["A", "B"] => "A and B"`
+ * - `["A", "B", "C"] => "A, B, and C"`
+ *
+ * **Why only nouns?**
+ *
+ * Some languages need a bit more context to translate other series.
+ *
+ * @param {String[]} items
+ */
+export const toNounSeriesText = items => {
+ if (items.length === 0) {
+ return '';
+ } else if (items.length === 1) {
+ return items[0];
+ } else if (items.length === 2) {
+ return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), {
+ firstItem: items[0],
+ lastItem: items[1],
+ });
+ }
+
+ return items.reduce((item, nextItem, idx) =>
+ idx === items.length - 1
+ ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem })
+ : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }),
+ );
+};
+
+export default {
+ toNounSeriesText,
+};
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index 14c02218990..37ad1676f7a 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -16,6 +16,7 @@ const httpStatusCodes = {
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
UNPROCESSABLE_ENTITY: 422,
diff --git a/app/assets/javascripts/lib/utils/icon_utils.js b/app/assets/javascripts/lib/utils/icon_utils.js
new file mode 100644
index 00000000000..7b8dd9bbef7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/icon_utils.js
@@ -0,0 +1,18 @@
+/* eslint-disable import/prefer-default-export */
+
+import axios from '~/lib/utils/axios_utils';
+
+/**
+ * Retrieve SVG icon path content from gitlab/svg sprite icons
+ * @param {String} name
+ */
+export const getSvgIconPathContent = name =>
+ axios
+ .get(gon.sprite_icons)
+ .then(({ data: svgs }) =>
+ new DOMParser()
+ .parseFromString(svgs, 'text/xml')
+ .querySelector(`#${name} path`)
+ .getAttribute('d'),
+ )
+ .catch(() => null);
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 2ccc51c35f7..19c4de6083d 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -80,3 +80,22 @@ export function numberToHumanSize(size) {
}
return `${bytesToGiB(size).toFixed(2)} GiB`;
}
+
+/**
+ * A simple method that returns the value of a + b
+ * It seems unessesary, but when combined with a reducer it
+ * adds up all the values in an array.
+ *
+ * e.g. `[1, 2, 3, 4, 5].reduce(sum) // => 15`
+ *
+ * @param {Float} a
+ * @param {Float} b
+ * @example
+ * // return 15
+ * [1, 2, 3, 4, 5].reduce(sum);
+ *
+ * // returns 6
+ * Object.values([{a: 1, b: 2, c: 3].reduce(sum);
+ * @returns {Float} The summed value
+ */
+export const sum = (a = 0, b = 0) => a + b;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 198711cf427..a900ff34bf5 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -63,6 +63,10 @@ export default class Poll {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && successCodes.indexOf(response.status) !== -1 && this.canPoll) {
+ if (this.timeoutID) {
+ clearTimeout(this.timeoutID);
+ }
+
this.timeoutID = setTimeout(() => {
this.makeRequest();
}, pollInterval);
@@ -101,15 +105,25 @@ export default class Poll {
}
/**
- * Restarts polling after it has been stoped
+ * Enables polling after it has been stopped
*/
- restart(options) {
- // update data
+ enable(options) {
if (options && options.data) {
this.options.data = options.data;
}
this.canPoll = true;
+
+ if (options && options.response) {
+ this.checkConditions(options.response);
+ }
+ }
+
+ /**
+ * Restarts polling after it has been stopped and makes a request
+ */
+ restart(options) {
+ this.enable(options);
this.makeRequest();
}
}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 1254ec798a6..84a617acb42 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -8,6 +8,10 @@ function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
}
+function addBlockTags(blockTag, selected) {
+ return `${blockTag}\n${selected}\n${blockTag}`;
+}
+
function lineBefore(text, textarea) {
var split;
split = text
@@ -24,19 +28,45 @@ function lineAfter(text, textarea) {
.split('\n')[0];
}
+function editorBlockTagText(text, blockTag, selected, editor) {
+ const lines = text.split('\n');
+ const selectionRange = editor.getSelectionRange();
+ const shouldRemoveBlock =
+ lines[selectionRange.start.row - 1] === blockTag &&
+ lines[selectionRange.end.row + 1] === blockTag;
+
+ if (shouldRemoveBlock) {
+ if (blockTag !== null) {
+ // ace is globally defined
+ // eslint-disable-next-line no-undef
+ const { Range } = ace.require('ace/range');
+ const lastLine = lines[selectionRange.end.row + 1];
+ const rangeWithBlockTags = new Range(
+ lines[selectionRange.start.row - 1],
+ 0,
+ selectionRange.end.row + 1,
+ lastLine.length,
+ );
+ editor.getSelection().setSelectionRange(rangeWithBlockTags);
+ }
+ return selected;
+ }
+ return addBlockTags(blockTag, selected);
+}
+
function blockTagText(text, textArea, blockTag, selected) {
- const before = lineBefore(text, textArea);
- const after = lineAfter(text, textArea);
- if (before === blockTag && after === blockTag) {
+ const shouldRemoveBlock =
+ lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag;
+
+ if (shouldRemoveBlock) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
- } else {
- return blockTag + '\n' + selected + '\n' + blockTag;
}
+ return addBlockTags(blockTag, selected);
}
function moveCursor({
@@ -46,33 +76,48 @@ function moveCursor({
positionBetweenTags,
removedLastNewLine,
select,
+ editor,
+ editorSelectionStart,
+ editorSelectionEnd,
}) {
var pos;
- if (!textArea.setSelectionRange) {
+ if (textArea && !textArea.setSelectionRange) {
return;
}
if (select && select.length > 0) {
- // calculate the part of the text to be selected
- const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
- const endPosition = startPosition + select.length;
- return textArea.setSelectionRange(startPosition, endPosition);
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (positionBetweenTags) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
+ if (textArea) {
+ // calculate the part of the text to be selected
+ const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
+ const endPosition = startPosition + select.length;
+ return textArea.setSelectionRange(startPosition, endPosition);
+ } else if (editor) {
+ editor.navigateLeft(tag.length - tag.indexOf(select));
+ editor.getSelection().selectAWord();
+ return;
}
+ }
+ if (textArea) {
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (positionBetweenTags) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
- if (removedLastNewLine) {
- pos -= 1;
- }
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
- if (cursorOffset) {
- pos -= cursorOffset;
- }
+ if (cursorOffset) {
+ pos -= cursorOffset;
+ }
- return textArea.setSelectionRange(pos, pos);
+ return textArea.setSelectionRange(pos, pos);
+ }
+ } else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
+ if (positionBetweenTags) {
+ editor.navigateLeft(tag.length);
+ }
}
}
@@ -85,6 +130,7 @@ export function insertMarkdownText({
selected = '',
wrap,
select,
+ editor,
}) {
var textToInsert,
selectedSplit,
@@ -92,11 +138,20 @@ export function insertMarkdownText({
removedLastNewLine,
removedFirstNewLine,
currentLineEmpty,
- lastNewLine;
+ lastNewLine,
+ editorSelectionStart,
+ editorSelectionEnd;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
+ if (editor) {
+ const selectionRange = editor.getSelectionRange();
+
+ editorSelectionStart = selectionRange.start;
+ editorSelectionEnd = selectionRange.end;
+ }
+
// check for link pattern and selected text is an URL
// if so fill in the url part instead of the text part of the pattern.
if (tag === LINK_TAG_PATTERN) {
@@ -119,14 +174,27 @@ export function insertMarkdownText({
}
// Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
+ if (textArea) {
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+ } else if (editor) {
+ if (editorSelectionStart.row !== editorSelectionEnd.row) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
}
selectedSplit = selected.split('\n');
- if (!wrap) {
+ if (editor && !wrap) {
+ lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
+
+ if (/^\s*$/.test(lastNewLine)) {
+ currentLineEmpty = true;
+ }
+ } else if (textArea && !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)
@@ -135,13 +203,19 @@ export function insertMarkdownText({
}
}
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+ const isBeginning =
+ (textArea && textArea.selectionStart === 0) ||
+ (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
+
+ startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
- textToInsert = blockTagText(text, textArea, blockTag, selected);
+ textToInsert = editor
+ ? editorBlockTagText(text, blockTag, selected, editor)
+ : blockTagText(text, textArea, blockTag, selected);
} else {
textToInsert = selectedSplit
.map(function(val) {
@@ -170,7 +244,11 @@ export function insertMarkdownText({
textToInsert += '\n';
}
- insertText(textArea, textToInsert);
+ if (editor) {
+ editor.insert(textToInsert);
+ } else {
+ insertText(textArea, textToInsert);
+ }
return moveCursor({
textArea,
tag: tag.replace(textPlaceholder, selected),
@@ -178,6 +256,9 @@ export function insertMarkdownText({
positionBetweenTags: wrap && selected.length === 0,
removedLastNewLine,
select,
+ editor,
+ editorSelectionStart,
+ editorSelectionEnd,
});
}
@@ -217,6 +298,25 @@ export function addMarkdownListeners(form) {
});
}
+export function addEditorMarkdownListeners(editor) {
+ $('.js-md')
+ .off('click')
+ .on('click', function(e) {
+ const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data();
+
+ insertMarkdownText({
+ tag: mdTag,
+ blockTag: mdBlock,
+ wrap: !mdPrepend,
+ select: mdSelect,
+ selected: editor.getSelectedText(),
+ text: editor.getValue(),
+ editor,
+ });
+ editor.focus();
+ });
+}
+
export function removeMarkdownListeners(form) {
return $('.js-md', form).off('click');
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 7cc7cd6d20e..c49b1bb5a2f 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -72,6 +72,29 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3
*/
export const truncateSha = sha => sha.substr(0, 8);
+const ELLIPSIS_CHAR = '…';
+export const truncatePathMiddleToLength = (text, maxWidth) => {
+ let returnText = text;
+ let ellipsisCount = 0;
+
+ while (returnText.length >= maxWidth) {
+ const textSplit = returnText.split('/').filter(s => s !== ELLIPSIS_CHAR);
+ const middleIndex = Math.floor(textSplit.length / 2);
+
+ returnText = textSplit
+ .slice(0, middleIndex)
+ .concat(
+ new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR),
+ textSplit.slice(middleIndex + 1),
+ )
+ .join('/');
+
+ ellipsisCount += 1;
+ }
+
+ return returnText;
+};
+
/**
* Capitalizes first character
*
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 9850f7ce782..4ba84589705 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -42,22 +42,35 @@ export function mergeUrlParams(params, url) {
return `${urlparts[1]}?${query}${urlparts[3]}`;
}
-export function removeParamQueryString(url, param) {
- const decodedUrl = decodeURIComponent(url);
- const urlVariables = decodedUrl.split('&');
-
- return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
-}
-
-export function removeParams(params, source = window.location.href) {
- const url = document.createElement('a');
- url.href = source;
+/**
+ * Removes specified query params from the url by returning a new url string that no longer
+ * includes the param/value pair. If no url is provided, `window.location.href` is used as
+ * the default value.
+ *
+ * @param {string[]} params - the query param names to remove
+ * @param {string} [url=windowLocation().href] - url from which the query param will be removed
+ * @returns {string} A copy of the original url but without the query param
+ */
+export function removeParams(params, url = window.location.href) {
+ const [rootAndQuery, fragment] = url.split('#');
+ const [root, query] = rootAndQuery.split('?');
+
+ if (!query) {
+ return url;
+ }
- params.forEach(param => {
- url.search = removeParamQueryString(url.search, param);
- });
+ const encodedParams = params.map(param => encodeURIComponent(param));
+ const updatedQuery = query
+ .split('&')
+ .filter(paramPair => {
+ const [foundParam] = paramPair.split('=');
+ return encodedParams.indexOf(foundParam) < 0;
+ })
+ .join('&');
- return url.href;
+ const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : '';
+ const writableFragment = fragment ? `#${fragment}` : '';
+ return `${root}${writableQuery}${writableFragment}`;
}
export function getLocationHash(url = window.location.href) {
@@ -66,6 +79,20 @@ export function getLocationHash(url = window.location.href) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}
+/**
+ * Apply the fragment to the given url by returning a new url string that includes
+ * the fragment. If the given url already contains a fragment, the original fragment
+ * will be removed.
+ *
+ * @param {string} url - url to which the fragment will be applied
+ * @param {string} fragment - fragment to append
+ */
+export const setUrlFragment = (url, fragment) => {
+ const [rootUrl] = url.split('#');
+ const encodedFragment = encodeURIComponent(fragment.replace(/^#/, ''));
+ return `${rootUrl}#${encodedFragment}`;
+};
+
export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
new file mode 100644
index 00000000000..308ad9784e4
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -0,0 +1,10 @@
+// tell webpack to load assets from origin so that web workers don't break
+// eslint-disable-next-line import/prefer-default-export
+export function resetServiceWorkersPublicPath() {
+ // __webpack_public_path__ is a global variable that can be used to adjust
+ // the webpack publicPath setting at runtime.
+ // see: https://webpack.js.org/guides/public-path/
+ const relativeRootPath = (gon && gon.relative_url_root) || '';
+ const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+}
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 5246c49842e..68b64a3a16a 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -4,8 +4,8 @@ import _ from 'underscore';
Very limited implementation of sprintf supporting only named parameters.
@param input (translated) text with parameters (e.g. '%{num_users} users use us')
- @param parameters object mapping parameter names to values (e.g. { num_users: 5 })
- @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
+ @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 })
+ @param {Boolean} escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
@returns {String} the text with parameters replaces (e.g. '5 users use us')
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c866e8d180a..1b722c0505a 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -66,15 +66,11 @@ gl.lazyLoader = new LazyLoader({
observerNode: '#content-body',
});
-document.addEventListener('DOMContentLoaded', () => {
+// Put all initialisations here that can also wait after everything is rendered and ready
+function deferredInitialisation() {
const $body = $('body');
- const $document = $(document);
- const $window = $(window);
- const $sidebarGutterToggle = $('.js-sidebar-toggle');
- let bootstrapBreakpoint = bp.getBreakpointSize();
initBreadcrumbs();
- initLayoutNav();
initImporterStatus();
initTodoToggle();
initLogoAnimation();
@@ -82,35 +78,6 @@ document.addEventListener('DOMContentLoaded', () => {
initUserPopovers();
if (document.querySelector('.search')) initSearchAutocomplete();
- if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
-
- // 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 clickHashLinkCallback() {
- const href = this.getAttribute('href');
- if (href.substr(1) === getLocationHash()) {
- setTimeout(handleLocationHash, 1);
- }
- });
-
- if (bootstrapBreakpoint === 'xs') {
- const $rightSidebar = $('aside.right-sidebar, .layout-page');
-
- $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- }
-
- // prevent default action for disabled buttons
- $('.btn').click(function clickDisabledButtonCallback(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
- }
-
- return true;
- });
addSelectOnFocusBehaviour('.js-select-on-focus');
@@ -132,27 +99,30 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Initialize select2 selects
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
-
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ if ($('select.select2').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ setTimeout(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ }, 1);
+ });
+ })
+ .catch(() => {});
+ }
// Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
trigger: 'hover',
boundary: 'viewport',
- placement(tip, el) {
- return $(el).data('placement') || 'bottom';
- },
});
// Initialize popovers
@@ -164,6 +134,50 @@ document.addEventListener('DOMContentLoaded', () => {
viewport: '.layout-page',
});
+ loadAwardsHandler();
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const $body = $('body');
+ const $document = $(document);
+ const $window = $(window);
+ const $sidebarGutterToggle = $('.js-sidebar-toggle');
+ let bootstrapBreakpoint = bp.getBreakpointSize();
+
+ if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
+
+ initLayoutNav();
+
+ // 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 clickHashLinkCallback() {
+ const href = this.getAttribute('href');
+ if (href.substr(1) === getLocationHash()) {
+ setTimeout(handleLocationHash, 1);
+ }
+ });
+
+ if (bootstrapBreakpoint === 'xs') {
+ const $rightSidebar = $('aside.right-sidebar, .layout-page');
+
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ }
+
+ // prevent default action for disabled buttons
+ $('.btn').click(function clickDisabledButtonCallback(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+
+ return true;
+ });
+
+ localTimeAgo($('abbr.timeago, .js-timeago'), true);
+
// Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() {
$(this)
@@ -171,8 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
.submit();
});
- localTimeAgo($('abbr.timeago, .js-timeago'), true);
-
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) {
const $buttons = $('[type="submit"], .js-disable-on-submit', this);
@@ -195,6 +207,10 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
+ $('.navbar-toggler').on('click', () => {
+ $('.header-content').toggleClass('menu-expanded');
+ });
+
// Commit show suppressed diff
$document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() {
const $container = $(this).parent();
@@ -202,10 +218,6 @@ document.addEventListener('DOMContentLoaded', () => {
$container.remove();
});
- $('.navbar-toggler').on('click', () => {
- $('.header-content').toggleClass('menu-expanded');
- });
-
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) {
const $this = $(this);
@@ -250,8 +262,6 @@ document.addEventListener('DOMContentLoaded', () => {
$window.on('resize.app', fitSidebarForSize);
- loadAwardsHandler();
-
$('form.filter-form').on('submit', function filterFormSubmitCallback(event) {
const link = document.createElement('a');
link.href = this.action;
@@ -274,4 +284,6 @@ document.addEventListener('DOMContentLoaded', () => {
// initialize field errors
$('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form));
+
+ requestIdleCallback(deferredInitialisation);
});
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0beedcacf33..0dabb28ea66 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
toggleClearInput.call($input);
},
+ firstDay: gon.first_day_of_week,
});
calendar.setDate(parsePikadayDate($input.val()));
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index bd263c75a3d..af2697444f2 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -16,25 +16,33 @@ export default class Members {
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
+ dropdownClicked(options) {
+ this.formSubmit(null, options.$el);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ dropdownToggleLabel(selected, $el) {
+ return $el.text();
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ dropdownIsSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ }
+
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
$btn.glDropdown({
selectable: true,
- isSelectable(selected, $el) {
- return !$el.hasClass('is-active');
- },
+ isSelectable: (selected, $el) => this.dropdownIsSelectable(selected, $el),
fieldName: $btn.data('fieldName'),
id(selected, $el) {
return $el.data('id');
},
- toggleLabel(selected, $el) {
- return $el.text();
- },
- clicked: options => {
- this.formSubmit(null, options.$el);
- },
+ toggleLabel: (selected, $el) => this.dropdownToggleLabel(selected, $el, $btn),
+ clicked: options => this.dropdownClicked(options),
});
});
}
@@ -55,6 +63,7 @@ export default class Members {
$toggle.enable();
$dateInput.enable();
}
+
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('elId')}`);
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 0deae478deb..3b42a154af8 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
+import createFlash from '~/flash';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
@@ -35,10 +36,18 @@ function MergeRequest(opts) {
dataType: 'merge_request',
fieldName: 'description',
selector: '.detail-page-description',
+ lockVersion: this.$el.data('lockVersion'),
onSuccess: result => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
},
+ onError: () => {
+ createFlash(
+ __(
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ ),
+ );
+ },
});
}
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b0dc5697018..2f15da42271 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -428,7 +428,7 @@ export default class MergeRequestTabs {
}
diffViewType() {
- return $('.inline-parallel-buttons button.active').data('viewType');
+ return $('.js-diff-view-buttons button.active').data('viewType');
}
isDiffAction(action) {
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 12224e36ba2..41783d311ef 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,18 +1,25 @@
<script>
-import { GlAreaChart } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+import { chartHeight, graphTypes, lineTypes } from '../../constants';
+
+let debouncedResize;
export default {
components: {
GlAreaChart,
+ Icon,
},
+ inheritAttrs: false,
props: {
graphData: {
type: Object,
required: true,
validator(data) {
return (
- data.queries &&
Array.isArray(data.queries) &&
data.queries.filter(query => {
if (Array.isArray(query.result)) {
@@ -25,19 +32,63 @@ export default {
);
},
},
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ alertData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: [],
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: chartHeight,
+ svgs: {},
+ primaryColor: null,
+ };
},
computed: {
chartData() {
- return this.graphData.queries.reduce((accumulator, query) => {
- const xLabel = `${query.unit}`;
- accumulator[xLabel] = {};
- query.result.forEach(res =>
- res.values.forEach(v => {
- accumulator[xLabel][v.time.toISOString()] = v.value;
- }),
- );
- return accumulator;
- }, {});
+ return this.graphData.queries.map(query => {
+ const { appearance } = query;
+ const lineType =
+ appearance && appearance.line && appearance.line.type
+ ? appearance.line.type
+ : lineTypes.default;
+ const lineColor = lineType === lineTypes.threshold ? this.primaryColor : undefined;
+
+ return {
+ name: this.formatLegendLabel(query),
+ data: this.concatenateResults(query.result),
+ lineStyle: {
+ type: lineType,
+ color: lineColor,
+ },
+ itemStyle: {
+ color: lineColor,
+ },
+ areaStyle: {
+ opacity:
+ appearance && appearance.area && typeof appearance.area.opacity === 'number'
+ ? appearance.area.opacity
+ : undefined,
+ },
+ };
+ });
},
chartOptions() {
return {
@@ -45,53 +96,172 @@ export default {
name: 'Time',
type: 'time',
axisLabel: {
- formatter: date => dateFormat(date, 'h:MMtt'),
+ formatter: date => dateFormat(date, 'h:MM TT'),
},
- nameTextStyle: {
- padding: [18, 0, 0, 0],
+ axisPointer: {
+ snap: true,
},
},
yAxis: {
- name: this.graphData.y_label,
+ name: this.yAxisLabel,
axisLabel: {
formatter: value => value.toFixed(3),
},
- nameTextStyle: {
- padding: [0, 0, 36, 0],
- },
},
- legend: {
- formatter: this.xAxisLabel,
+ series: this.scatterSeries,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
+ dataZoomConfig() {
+ const handleIcon = this.svgs['scroll-handle'];
+
+ return handleIcon ? { handleIcon } : {};
+ },
+ earliestDatapoint() {
+ return this.chartData.reduce((acc, series) => {
+ const [[timestamp]] = series.data.sort(([a], [b]) => {
+ if (a < b) {
+ return -1;
+ }
+ return a > b ? 1 : 0;
+ });
+
+ return timestamp < acc || acc === null ? timestamp : acc;
+ }, null);
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ acc.push({
+ id: deployment.id,
+ createdAt: deployment.created_at,
+ sha: deployment.sha,
+ commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
+ tag: deployment.tag,
+ tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
+ ref: deployment.ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: graphTypes.deploymentData,
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.svgs.rocket,
+ symbolSize: 14,
+ itemStyle: {
+ color: this.primaryColor,
},
};
},
- xAxisLabel() {
- return this.graphData.queries.map(query => query.label).join(', ');
+ yAxisLabel() {
+ return `${this.graphData.y_label}`;
},
},
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.setSvg('rocket');
+ this.setSvg('scroll-handle');
+ },
methods: {
+ concatenateResults(results) {
+ return results.reduce((acc, result) => acc.concat(result.values), []);
+ },
+ formatLegendLabel(query) {
+ return `${query.label}`;
+ },
formatTooltipText(params) {
- const [date, value] = params;
- return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)];
+ this.tooltip.title = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
+ this.tooltip.content = [];
+ params.seriesData.forEach(seriesData => {
+ if (seriesData.componentSubType === graphTypes.deploymentData) {
+ this.tooltip.isDeployment = true;
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === seriesData.value[0],
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ } else {
+ const { seriesName } = seriesData;
+ // seriesData.value contains the chart's [x, y] value pair
+ // seriesData.value[1] is threfore the chart y value
+ const value = seriesData.value[1].toFixed(3);
+
+ this.tooltip.content.push({
+ name: seriesName,
+ value,
+ });
+ }
+ });
+ },
+ setSvg(name) {
+ getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(() => {});
+ },
+ onChartUpdated(chart) {
+ [this.primaryColor] = chart.getOption().color;
},
- onCreated(chart) {
- this.$emit('created', chart);
+ onResize() {
+ const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
+ this.width = width;
},
},
};
</script>
<template>
- <div class="prometheus-graph">
+ <div class="prometheus-graph col-12 col-lg-6">
<div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
+ ref="areaChart"
+ v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
- @created="onCreated"
- />
+ :thresholds="alertData"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template slot="tooltipTitle">
+ <div v-if="tooltip.isDeployment">
+ {{ __('Deployed') }}
+ </div>
+ {{ tooltip.title }}
+ </template>
+ <template slot="tooltipContent">
+ <div v-if="tooltip.isDeployment" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ {{ tooltip.sha }}
+ </div>
+ <template v-else>
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ {{ content.name }} {{ content.value }}
+ </div>
+ </template>
+ </template>
+ </gl-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2d9c5050c9b..895a57785bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,20 +1,19 @@
<script>
-import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
-import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
-import eventHub from '../event_hub';
+
+const sidebarAnimationDuration = 150;
+let sidebarMutationObserver;
export default {
components: {
MonitorAreaChart,
- Graph,
GraphGroup,
EmptyState,
Icon,
@@ -25,21 +24,11 @@ export default {
required: false,
default: true,
},
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
showPanels: {
type: Boolean,
required: false,
default: true,
},
- forceSmallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
documentationPath: {
type: String,
required: true,
@@ -99,51 +88,38 @@ export default {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
- hoverData: {},
elWidth: 0,
};
},
- computed: {
- graphComponent() {
- return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph;
- },
- forceRedraw() {
- return this.elWidth;
- },
- },
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
- this.mutationObserverConfig = {
- attributes: true,
- childList: false,
- subtree: false,
- };
- eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
- eventHub.$off('hoverChanged', this.hoverChanged);
- window.removeEventListener('resize', this.resizeThrottled, false);
- this.sidebarMutationObserver.disconnect();
+ if (sidebarMutationObserver) {
+ sidebarMutationObserver.disconnect();
+ }
},
mounted() {
- this.resizeThrottled = _.debounce(this.resize, 100);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
- window.addEventListener('resize', this.resizeThrottled, false);
-
- const sidebarEl = document.querySelector('.nav-sidebar');
- // The sidebar listener
- this.sidebarMutationObserver = new MutationObserver(this.resizeThrottled);
- this.sidebarMutationObserver.observe(sidebarEl, this.mutationObserverConfig);
+ sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
+ sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
+ attributes: true,
+ childList: false,
+ subtree: false,
+ });
}
},
methods: {
+ getGraphAlerts(graphId) {
+ return this.alertData ? this.alertData[graphId] || {} : {};
+ },
getGraphsData() {
this.state = 'loading';
Promise.all([
@@ -165,42 +141,40 @@ export default {
this.showEmptyState = false;
})
- .then(this.resize)
.catch(() => {
this.state = 'unableToConnect';
});
},
- resize() {
- this.elWidth = this.$el.clientWidth;
- },
- hoverChanged(data) {
- this.hoverData = data;
+ onSidebarMutation() {
+ setTimeout(() => {
+ this.elWidth = this.$el.clientWidth;
+ }, sidebarAnimationDuration);
},
},
};
</script>
<template>
- <div v-if="!showEmptyState" :key="forceRedraw" class="prometheus-graphs prepend-top-default">
+ <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
- <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" />
+ <span>{{ currentEnvironmentName }}</span>
+ <icon name="chevron-down" />
</button>
<div
v-if="store.environmentsData.length > 0"
class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
>
<ul>
- <li v-for="environment in store.environmentsData" :key="environment.latest.id">
+ <li v-for="environment in store.environmentsData" :key="environment.id">
<a
- :href="environment.latest.metrics_path"
- :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ :href="environment.metrics_path"
+ :class="{ 'is-active': environment.name == currentEnvironmentName }"
class="dropdown-item"
+ >{{ environment.name }}</a
>
- {{ environment.latest.name }}
- </a>
</li>
</ul>
</div>
@@ -212,21 +186,15 @@ export default {
:name="groupData.group"
:show-panels="showPanels"
>
- <component
- :is="graphComponent"
+ <monitor-area-chart
v-for="(graphData, graphIndex) in groupData.metrics"
:key="graphIndex"
:graph-data="graphData"
- :hover-data="hoverData"
:deployment-data="store.deploymentData"
- :project-path="projectPath"
- :tags-path="tagsPath"
- :show-legend="showLegend"
- :small-graph="forceSmallGraph"
- >
- <!-- EE content -->
- {{ null }}
- </component>
+ :alert-data="getGraphAlerts(graphData.id)"
+ :container-width="elWidth"
+ group-id="monitor-area-chart"
+ />
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
deleted file mode 100644
index 64a1df80a8e..00000000000
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ /dev/null
@@ -1,329 +0,0 @@
-<script>
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { axisLeft, axisBottom } from 'd3-axis';
-import _ from 'underscore';
-import { max, extent } from 'd3-array';
-import { select } from 'd3-selection';
-import GraphAxis from './graph/axis.vue';
-import GraphLegend from './graph/legend.vue';
-import GraphFlag from './graph/flag.vue';
-import GraphDeployment from './graph/deployment.vue';
-import GraphPath from './graph/path.vue';
-import MonitoringMixin from '../mixins/monitoring_mixins';
-import eventHub from '../event_hub';
-import measurements from '../utils/measurements';
-import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
-import createTimeSeries from '../utils/multiple_time_series';
-import bp from '../../breakpoints';
-
-const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
-
-export default {
- components: {
- GraphAxis,
- GraphFlag,
- GraphDeployment,
- GraphPath,
- GraphLegend,
- },
- mixins: [MonitoringMixin],
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- hoverData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- smallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- baseGraphHeight: 450,
- baseGraphWidth: 600,
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- margin: {},
- unitOfDisplay: '',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentXCoordinate: 0,
- currentCoordinates: {},
- showFlag: false,
- showFlagContent: false,
- timeSeries: [],
- graphDrawData: {},
- realPixelRatio: 1,
- seriesUnderMouse: [],
- };
- },
- computed: {
- outerViewBox() {
- return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
- },
- innerViewBox() {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- },
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
- };
- },
- deploymentFlagData() {
- return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
- },
- shouldRenderData() {
- return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
- },
- },
- watch: {
- hoverData() {
- this.positionFlag();
- },
- },
- mounted() {
- this.draw();
- },
- methods: {
- showDot(path) {
- return this.showFlagContent && this.seriesUnderMouse.includes(path);
- },
- draw() {
- const breakpointSize = bp.getBreakpointSize();
- const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
-
- this.margin = measurements.large.margin;
-
- if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
-
- this.yAxisLabel = this.graphData.y_label || 'Values';
- this.graphWidth = svgWidth - this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight - 50;
- this.baseGraphWidth = this.graphWidth;
-
- // pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = svgWidth / this.baseGraphWidth;
-
- // set the legends on the axes
- const [query] = this.graphData.queries;
- this.legendTitle = query ? query.label : 'Average';
- this.unitOfDisplay = query ? query.unit : '';
-
- if (this.shouldRenderData) {
- this.renderAxesPaths();
- this.formatDeployments();
- }
- },
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x += 7;
-
- this.seriesUnderMouse = this.timeSeries.filter(series => {
- const mouseX = series.timeSeriesScaleX.invert(point.x);
- let minDistance = Infinity;
-
- const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
- const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
- if (distance < minDistance) {
- minDistance = distance;
- return x;
- }
- return closest;
- });
-
- return series.values.find(v => v.time.toString() === closestTickMark);
- });
-
- const firstTimeSeries = this.seriesUnderMouse[0];
- const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
- const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
- const d0 = firstTimeSeries.values[overlayIndex - 1];
- const d1 = firstTimeSeries.values[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
- const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
-
- eventHub.$emit('hoverChanged', {
- hoveredDate,
- currentDeployXPos,
- });
- },
- renderAxesPaths() {
- ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
- this.graphData.queries,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset,
- ));
-
- if (_.findWhere(this.timeSeries, { renderCanary: true })) {
- this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
- }
-
- const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
- const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
-
- const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
- axisXScale.domain(d3.extent(allValues, d => d.time));
- axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
-
- this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
- const seriesKeys = {};
- series.values.forEach(v => {
- seriesKeys[v.time] = true;
- });
- return {
- ...obj,
- ...seriesKeys,
- };
- }, {});
-
- const xAxis = d3
- .axisBottom()
- .scale(axisXScale)
- .ticks(this.graphWidth / 120)
- .tickFormat(timeScaleFormat);
-
- const yAxis = d3
- .axisLeft()
- .scale(axisYScale)
- .ticks(measurements.yTicks);
-
- d3.select(this.$refs.baseSvg)
- .select('.x-axis')
- .call(xAxis);
-
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg)
- .select('.y-axis')
- .call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this)
- .select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
- },
- },
-};
-</script>
-
-<template>
- <div
- class="prometheus-graph"
- @mouseover="showFlagContent = true;"
- @mouseleave="showFlagContent = false;"
- >
- <div class="prometheus-graph-header">
- <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div class="prometheus-graph-widgets"><slot></slot></div>
- </div>
- <div :style="paddingBottomRootSvg" class="prometheus-svg-container">
- <svg ref="baseSvg" :viewBox="outerViewBox">
- <g :transform="axisTransform" class="x-axis" />
- <g class="y-axis" transform="translate(70, 20)" />
- <graph-axis
- :graph-width="graphWidth"
- :graph-height="graphHeight"
- :margin="margin"
- :measurements="measurements"
- :y-axis-label="yAxisLabel"
- :unit-of-display="unitOfDisplay"
- />
- <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
- <slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
- <graph-path
- v-for="(path, index) in timeSeries"
- :key="index"
- :generated-line-path="path.linePath"
- :generated-area-path="path.areaPath"
- :line-style="path.lineStyle"
- :line-color="path.lineColor"
- :area-color="path.areaColor"
- :current-coordinates="currentCoordinates[path.metricTag]"
- :show-dot="showDot(path)"
- />
- <graph-deployment
- :deployment-data="reducedDeploymentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- <rect
- ref="graphOverlay"
- :width="graphWidth - 70"
- :height="graphHeight - 100"
- class="prometheus-graph-overlay"
- transform="translate(-5, 20)"
- @mousemove="handleMouseOverGraph($event);"
- />
- </svg>
- <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
- <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
- {{ s__('Metrics|No data to display') }}
- </text>
- </svg>
- </svg>
- <graph-flag
- v-if="shouldRenderData"
- :real-pixel-ratio="realPixelRatio"
- :current-x-coordinate="currentXCoordinate"
- :current-data="currentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- :show-flag-content="showFlagContent"
- :time-series="seriesUnderMouse"
- :unit-of-display="unitOfDisplay"
- :legend-title="legendTitle"
- :deployment-flag-data="deploymentFlagData"
- :current-coordinates="currentCoordinates"
- />
- </div>
- <graph-legend v-if="showLegend" :legend-title="legendTitle" :time-series="timeSeries" />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
deleted file mode 100644
index 8f046857a20..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/axis.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<script>
-import { convertToSentenceCase } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
-
-export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate =
- (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
- this.yLabelWidth / 2 || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
- },
-
- yPosition() {
- return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
- },
-
- yAxisLabelSentenceCase() {
- return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
- },
-
- timeString() {
- return s__('PrometheusDashboard|Time');
- },
- },
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
-};
-</script>
-<template>
- <g class="axis-label-container">
- <line
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition"
- class="label-x-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- />
- <line
- :x2="10"
- :y2="yPosition"
- class="label-y-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- y1="0"
- />
- <rect
- :transform="rectTransform"
- :width="yLabelWidth"
- :height="yLabelHeight"
- class="rect-axis-text"
- />
- <text
- ref="ylabel"
- :transform="textTransform"
- class="label-axis-text y-label-text"
- text-anchor="middle"
- >
- {{ yAxisLabelSentenceCase }}
- </text>
- <rect :x="xPosition + 60" :y="graphHeight - 80" class="rect-axis-text" width="35" height="50" />
- <text :x="xPosition + 60" :y="yPosition" class="label-axis-text x-label-text" dy=".35em">
- {{ timeString }}
- </text>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
deleted file mode 100644
index bee9784692c..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-export default {
- props: {
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- },
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
- },
- methods: {
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
- },
- },
-};
-</script>
-<template>
- <g class="deploy-info">
- <g
- v-for="(deployment, index) in deploymentData"
- :key="index"
- :transform="transformDeploymentGroup(deployment)"
- >
- <rect :height="calculatedHeight" x="0" y="0" width="3" fill="url(#shadow-gradient)" />
- <line :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" stroke="#000" />
- </g>
- <svg height="0" width="0">
- <defs>
- <linearGradient id="shadow-gradient">
- <stop offset="0%" stop-color="#000" stop-opacity="0.4" />
- <stop offset="100%" stop-color="#000" stop-opacity="0" />
- </linearGradient>
- </defs>
- </svg>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
deleted file mode 100644
index 9d6d1caef80..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<script>
-import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
-import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-import Icon from '../../../vue_shared/components/icon.vue';
-import TrackLine from './track_line.vue';
-
-export default {
- components: {
- Icon,
- TrackLine,
- },
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- deploymentFlagData: {
- type: Object,
- required: false,
- default: null,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- realPixelRatio: {
- type: Number,
- required: true,
- },
- showFlagContent: {
- type: Boolean,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: true,
- },
- },
- computed: {
- formatTime() {
- return this.deploymentFlagData
- ? timeFormat(this.deploymentFlagData.time)
- : timeFormat(this.currentData.time);
- },
- formatDate() {
- return this.deploymentFlagData
- ? dateFormat(this.deploymentFlagData.time)
- : dateFormat(this.currentData.time);
- },
- cursorStyle() {
- const xCoordinate = this.deploymentFlagData
- ? this.deploymentFlagData.xPos
- : this.currentXCoordinate;
-
- const offsetTop = 20 * this.realPixelRatio;
- const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
- const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
-
- return {
- top: `${offsetTop}px`,
- left: `${offsetLeft}px`,
- height: `${height}px`,
- };
- },
- flagOrientation() {
- if (this.currentXCoordinate * this.realPixelRatio > 120) {
- return 'left';
- }
- return 'right';
- },
- },
- methods: {
- seriesMetricValue(seriesIndex, series) {
- const indexFromCoordinates = this.currentCoordinates[series.metricTag]
- ? this.currentCoordinates[series.metricTag].currentDataIndex
- : 0;
- const index = this.deploymentFlagData
- ? this.deploymentFlagData.seriesIndex
- : indexFromCoordinates;
- const value = series.values[index] && series.values[index].value;
- if (Number.isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
- },
- seriesMetricLabel(index, series) {
- if (this.timeSeries.length < 2) {
- return this.legendTitle;
- }
- if (series.metricTag) {
- return series.metricTag;
- }
- return `series ${index + 1}`;
- },
- },
-};
-</script>
-
-<template>
- <div :style="cursorStyle" class="prometheus-graph-cursor">
- <div v-if="showFlagContent" :class="flagOrientation" class="prometheus-graph-flag popover">
- <div class="arrow-shadow"></div>
- <div class="arrow"></div>
- <div class="popover-title">
- <h5 v-if="deploymentFlagData">Deployed</h5>
- {{ formatDate }} <strong>{{ formatTime }}</strong>
- </div>
- <div v-if="deploymentFlagData" class="popover-content deploy-meta-content">
- <div>
- <icon :size="12" name="commit" />
- <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} </a>
- </div>
- <div v-if="deploymentFlagData.tag">
- <icon :size="12" name="label" />
- <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} </a>
- </div>
- </div>
- <div class="popover-content">
- <table class="prometheus-table">
- <tr v-for="(series, index) in timeSeries" :key="index">
- <track-line :track="series" />
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
- <td>
- <strong>{{ seriesMetricValue(index, series) }}</strong>
- </td>
- </tr>
- </table>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
deleted file mode 100644
index b5211c306a3..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import TrackLine from './track_line.vue';
-import TrackInfo from './track_info.vue';
-
-export default {
- components: {
- TrackLine,
- TrackInfo,
- },
- props: {
- legendTitle: {
- type: String,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- },
- methods: {
- isStable(track) {
- return {
- 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
- };
- },
- },
-};
-</script>
-<template>
- <div class="prometheus-graph-legends prepend-left-10">
- <table class="prometheus-table">
- <tr
- v-for="(series, index) in timeSeries"
- v-if="series.shouldRenderLegend"
- :key="index"
- :class="isStable(series)"
- >
- <td>
- <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
- </td>
- <track-line :track="series" />
- <td v-if="timeSeries.length > 1" class="legend-metric-title">
- <track-info v-if="series.metricTag" :track="series" />
- <track-info v-else :track="series">
- <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
- </track-info>
- </td>
- <td v-else>
- <track-info :track="series">
- <strong>{{ legendTitle }}</strong>
- </track-info>
- </td>
- <template v-for="(track, trackIndex) in series.tracksLegend">
- <track-line :key="`track-line-${trackIndex}`" :track="track" />
- <td :key="`track-info-${trackIndex}`">
- <track-info :track="track" class="legend-metric-title" />
- </td>
- </template>
- </tr>
- </table>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
deleted file mode 100644
index f2c237ec391..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-export default {
- props: {
- generatedLinePath: {
- type: String,
- required: true,
- },
- generatedAreaPath: {
- type: String,
- required: true,
- },
- lineStyle: {
- type: String,
- required: false,
- default: '',
- },
- lineColor: {
- type: String,
- required: true,
- },
- areaColor: {
- type: String,
- required: true,
- },
- currentCoordinates: {
- type: Object,
- required: false,
- default: () => ({ currentX: 0, currentY: 0 }),
- },
- showDot: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- strokeDashArray() {
- if (this.lineStyle === 'dashed') return '3, 1';
- if (this.lineStyle === 'dotted') return '1, 1';
- return null;
- },
- },
-};
-</script>
-<template>
- <g transform="translate(-5, 20)">
- <circle
- v-if="showDot"
- :cx="currentCoordinates.currentX"
- :cy="currentCoordinates.currentY"
- :fill="lineColor"
- :stroke="lineColor"
- class="circle-path"
- r="3"
- />
- <path :d="generatedAreaPath" :fill="areaColor" class="metric-area" />
- <path
- :d="generatedLinePath"
- :stroke="lineColor"
- :stroke-dasharray="strokeDashArray"
- class="metric-line"
- fill="none"
- stroke-width="1"
- />
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue
deleted file mode 100644
index 3464067834f..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_info.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { formatRelevantDigits } from '~/lib/utils/number_utils';
-
-export default {
- name: 'TrackInfo',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- summaryMetrics() {
- return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
- this.track.max,
- )}`;
- },
- },
-};
-</script>
-<template>
- <span>
- <slot>
- <strong> {{ track.metricTag }} </strong>
- </slot>
- {{ summaryMetrics }}
- </span>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
deleted file mode 100644
index d2ed1ba113e..00000000000
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-export default {
- name: 'TrackLine',
- props: {
- track: {
- type: Object,
- required: true,
- },
- },
- computed: {
- stylizedLine() {
- if (this.track.lineStyle === 'dashed') return '6, 3';
- if (this.track.lineStyle === 'dotted') return '3, 3';
- return null;
- },
- },
-};
-</script>
-<template>
- <td>
- <svg width="16" height="8">
- <line
- :stroke-dasharray="stylizedLine"
- :stroke="track.lineColor"
- :x1="0"
- :x2="16"
- :y1="4"
- :y2="4"
- stroke-width="4"
- />
- </svg>
- </td>
-</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
new file mode 100644
index 00000000000..869173b6572
--- /dev/null
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -0,0 +1,10 @@
+export const chartHeight = 300;
+
+export const graphTypes = {
+ deploymentData: 'scatter',
+};
+
+export const lineTypes = {
+ default: 'solid',
+ threshold: 'dashed',
+};
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
deleted file mode 100644
index 87c3d969de4..00000000000
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { bisectDate } from '../utils/date_time_formatters';
-
-const mixins = {
- methods: {
- mouseOverDeployInfo(mouseXPos) {
- if (!this.reducedDeploymentData) return false;
-
- let dataFound = false;
- this.reducedDeploymentData = this.reducedDeploymentData.map(d => {
- const deployment = d;
- if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
- dataFound = d.xPos + 1;
-
- deployment.showDeploymentFlag = true;
- } else {
- deployment.showDeploymentFlag = false;
- }
- return deployment;
- });
-
- return dataFound;
- },
-
- formatDeployments() {
- this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
- const time = new Date(deployment.created_at);
- const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
-
- time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
-
- if (xPos >= 0) {
- const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1);
-
- deploymentDataArray.push({
- id: deployment.id,
- time,
- sha: deployment.sha,
- commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
- tag: deployment.tag,
- tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null,
- ref: deployment.ref.name,
- xPos,
- seriesIndex,
- showDeploymentFlag: false,
- });
- }
-
- return deploymentDataArray;
- }, []);
- },
-
- positionFlag() {
- const timeSeries = this.seriesUnderMouse[0];
- if (!timeSeries) {
- return;
- }
- const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
-
- this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
-
- this.currentCoordinates = {};
-
- this.seriesUnderMouse.forEach(series => {
- const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
- const currentData = series.values[currentDataIndex];
- const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
- const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
-
- this.currentCoordinates[series.metricTag] = {
- currentX,
- currentY,
- currentDataIndex,
- };
- });
-
- if (this.hoverData.currentDeployXPos) {
- this.showFlag = false;
- } else {
- this.showFlag = true;
- }
- },
- },
-};
-
-export default mixins;
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 8692c873a41..70635059bd9 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -13,7 +13,7 @@ function checkQueryEmptyData(query) {
result: query.result.filter(timeSeries => {
const newTimeSeries = timeSeries;
const hasValue = series =>
- !Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
+ !Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
const hasNonNullValue = timeSeries.values.find(hasValue);
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
@@ -33,10 +33,10 @@ function normalizeMetrics(metrics) {
...query,
result: query.result.map(result => ({
...result,
- values: result.values.map(([timestamp, value]) => ({
- time: new Date(timestamp * 1000),
- value: Number(value),
- })),
+ values: result.values.map(([timestamp, value]) => [
+ new Date(timestamp * 1000).toISOString(),
+ Number(value),
+ ]),
})),
}));
@@ -66,9 +66,7 @@ export default class MonitoringStore {
}
storeEnvironmentsData(environmentsData = []) {
- this.environmentsData = environmentsData.filter(
- environment => !!environment.latest.last_deployment,
- );
+ this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment);
}
getMetricsCount() {
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
deleted file mode 100644
index d88c13609dc..00000000000
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { timeFormat as time } from 'd3-time-format';
-import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
-import { bisector } from 'd3-array';
-
-const d3 = {
- time,
- bisector,
- timeSecond,
- timeMinute,
- timeHour,
- timeDay,
- timeWeek,
- timeMonth,
- timeYear,
-};
-
-export const dateFormat = d3.time('%d %b %Y, ');
-export const timeFormat = d3.time('%-I:%M%p');
-export const dateFormatWithName = d3.time('%a, %b %-d');
-export const bisectDate = d3.bisector(d => d.time).left;
-
-export function timeScaleFormat(date) {
- let formatFunction;
- if (d3.timeSecond(date) < date) {
- formatFunction = d3.time('.%L');
- } else if (d3.timeMinute(date) < date) {
- formatFunction = d3.time(':%S');
- } else if (d3.timeHour(date) < date) {
- formatFunction = d3.time('%-I:%M');
- } else if (d3.timeDay(date) < date) {
- formatFunction = d3.time('%-I %p');
- } else if (d3.timeWeek(date) < date) {
- formatFunction = d3.time('%a %d');
- } else if (d3.timeMonth(date) < date) {
- formatFunction = d3.time('%b %d');
- } else if (d3.timeYear(date) < date) {
- formatFunction = d3.time('%B');
- } else {
- formatFunction = d3.time('%Y');
- }
- return formatFunction(date);
-}
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
deleted file mode 100644
index 7c771f43eee..00000000000
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ /dev/null
@@ -1,44 +0,0 @@
-export default {
- small: {
- // Covers both xs and sm screen sizes
- margin: {
- top: 40,
- right: 40,
- bottom: 50,
- left: 40,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 32,
- },
- backgroundLegend: {
- width: 30,
- height: 50,
- },
- axisLabelLineOffset: -20,
- },
- large: {
- // This covers both md and lg screen sizes
- margin: {
- top: 80,
- right: 80,
- bottom: 100,
- left: 80,
- },
- legends: {
- width: 15,
- height: 3,
- offsetX: 20,
- offsetY: 34,
- },
- backgroundLegend: {
- width: 30,
- height: 150,
- },
- axisLabelLineOffset: 20,
- },
- xTicks: 8,
- yTicks: 3,
-};
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
deleted file mode 100644
index 50ba14dfb2e..00000000000
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ /dev/null
@@ -1,223 +0,0 @@
-import _ from 'underscore';
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { line, area, curveLinear } from 'd3-shape';
-import { extent, max, sum } from 'd3-array';
-import { timeMinute, timeSecond } from 'd3-time';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-
-const d3 = {
- scaleLinear,
- scaleTime,
- line,
- area,
- curveLinear,
- extent,
- max,
- timeMinute,
- timeSecond,
- sum,
-};
-
-const defaultColorPalette = {
- blue: ['#1f78d1', '#8fbce8'],
- orange: ['#fc9403', '#feca81'],
- red: ['#db3b21', '#ed9d90'],
- green: ['#1aaa55', '#8dd5aa'],
- purple: ['#6666c4', '#d1d1f0'],
-};
-
-const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-
-const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
-
-function queryTimeSeries(query, graphDrawData, lineStyle) {
- let usedColors = [];
- let renderCanary = false;
- const timeSeriesParsed = [];
-
- function pickColor(name) {
- let pick;
- if (name && defaultColorPalette[name]) {
- pick = name;
- } else {
- const unusedColors = _.difference(defaultColorOrder, usedColors);
- if (unusedColors.length > 0) {
- [pick] = unusedColors;
- } else {
- usedColors = [];
- [pick] = defaultColorOrder;
- }
- }
- usedColors.push(pick);
- return defaultColorPalette[pick];
- }
-
- function findByDate(series, time) {
- const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
- if (val) {
- return val.value;
- }
- return NaN;
- }
-
- // The timeseries data may have gaps in it
- // but we need a regularly-spaced set of time/value pairs
- // this gives us a complete range of one minute intervals
- // offset the same amount as the original data
- const [minX, maxX] = graphDrawData.xDom;
- const offset = d3.timeMinute(minX) - Number(minX);
- const datesWithoutGaps = d3.timeSecond
- .every(60)
- .range(d3.timeMinute.offset(minX, -1), maxX)
- .map(d => d - offset);
-
- query.result.forEach((timeSeries, timeSeriesNumber) => {
- let metricTag = '';
- let lineColor = '';
- let areaColor = '';
- let shouldRenderLegend = true;
- const timeSeriesValues = timeSeries.values.map(d => d.value);
- const maximumValue = d3.max(timeSeriesValues);
- const accum = d3.sum(timeSeriesValues);
- const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
-
- if (trackName === 'Canary') {
- renderCanary = true;
- }
-
- const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData =
- query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
-
- if (seriesCustomizationData) {
- metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
- [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
- if (timeSeriesParsed.length > 0) {
- shouldRenderLegend = false;
- } else {
- shouldRenderLegend = true;
- }
- } else {
- metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
- [lineColor, areaColor] = pickColor();
- if (timeSeriesParsed.length > 1) {
- shouldRenderLegend = false;
- }
- }
-
- const values = datesWithoutGaps.map(time => ({
- time,
- value: findByDate(timeSeries.values, time),
- }));
-
- timeSeriesParsed.push({
- linePath: graphDrawData.lineFunction(values),
- areaPath: graphDrawData.areaBelowLine(values),
- timeSeriesScaleX: graphDrawData.timeSeriesScaleX,
- timeSeriesScaleY: graphDrawData.timeSeriesScaleY,
- values: timeSeries.values,
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- areaColor,
- metricTag,
- trackName,
- shouldRenderLegend,
- renderCanary,
- });
-
- if (!shouldRenderLegend) {
- if (!timeSeriesParsed[0].tracksLegend) {
- timeSeriesParsed[0].tracksLegend = [];
- }
- timeSeriesParsed[0].tracksLegend.push({
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- metricTag,
- });
- }
- });
-
- return timeSeriesParsed;
-}
-
-function xyDomain(queries) {
- const allValues = queries.reduce(
- (allQueryResults, query) =>
- allQueryResults.concat(
- query.result.reduce((allResults, result) => allResults.concat(result.values), []),
- ),
- [],
- );
-
- const xDom = d3.extent(allValues, d => d.time);
- const yDom = [0, d3.max(allValues.map(d => d.value))];
-
- return {
- xDom,
- yDom,
- };
-}
-
-export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) {
- const { xDom, yDom } = xyDomain(queries);
-
- const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
-
- timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.timeMinute, 60);
- timeSeriesScaleY.domain(yDom);
-
- const defined = d => !Number.isNaN(d.value) && d.value != null;
-
- const lineFunction = d3
- .line()
- .defined(defined)
- .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
- .x(d => timeSeriesScaleX(d.time))
- .y(d => timeSeriesScaleY(d.value));
-
- const areaBelowLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value));
-
- const areaAboveLine = d3
- .area()
- .defined(defined)
- .curve(d3.curveLinear)
- .x(d => timeSeriesScaleX(d.time))
- .y0(0)
- .y1(d => timeSeriesScaleY(d.value));
-
- return {
- lineFunction,
- areaBelowLine,
- areaAboveLine,
- xDom,
- yDom,
- timeSeriesScaleX,
- timeSeriesScaleY,
- };
-}
-
-export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
- const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset);
-
- const timeSeries = queries.reduce((series, query, index) => {
- const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
- return series.concat(queryTimeSeries(query, graphDrawData, lineStyle));
- }, []);
-
- return {
- timeSeries,
- graphDrawData,
- };
-}
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index e4d72eb8318..8eccba07c38 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,82 +1,22 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { mapActions, mapState, mapGetters } from 'vuex';
+import store from 'ee_else_ce/mr_notes/stores';
+import initNotesApp from './init_notes';
import initDiffsApp from '../diffs';
-import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
import initDiscussionFilters from '../notes/discussion_filters';
-import store from './stores';
import MergeRequest from '../merge_request';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
export default function initMrNotes() {
+ resetServiceWorkersPublicPath();
+
const mrShowNode = document.querySelector('.merge-request');
// eslint-disable-next-line no-new
new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
- // eslint-disable-next-line no-new
- new Vue({
- el: '#js-vue-mr-discussions',
- name: 'MergeRequestDiscussions',
- components: {
- notesApp,
- },
- store,
- data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
- const noteableData = JSON.parse(notesDataset.noteableData);
- noteableData.noteableType = notesDataset.noteableType;
- noteableData.targetType = notesDataset.targetType;
-
- return {
- noteableData,
- currentUserData: JSON.parse(notesDataset.currentUserData),
- notesData: JSON.parse(notesDataset.notesData),
- helpPagePath: notesDataset.helpPagePath,
- };
- },
- computed: {
- ...mapGetters(['discussionTabCounter']),
- ...mapState({
- activeTab: state => state.page.activeTab,
- }),
- },
- watch: {
- discussionTabCounter() {
- this.updateDiscussionTabCounter();
- },
- },
- created() {
- this.setActiveTab(window.mrTabs.getCurrentAction());
- },
- mounted() {
- this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
- $(document).on('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
- },
- beforeDestroy() {
- $(document).off('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
- },
- methods: {
- ...mapActions(['setActiveTab']),
- updateDiscussionTabCounter() {
- this.notesCountBadge.text(this.discussionTabCounter);
- },
- },
- render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
- shouldShow: this.activeTab === 'show',
- helpPagePath: this.helpPagePath,
- },
- });
- },
- });
+ initNotesApp();
// eslint-disable-next-line no-new
new Vue({
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
new file mode 100644
index 00000000000..842a209a545
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -0,0 +1,70 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import store from 'ee_else_ce/mr_notes/stores';
+import notesApp from '../notes/components/notes_app.vue';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-vue-mr-discussions',
+ name: 'MergeRequestDiscussions',
+ components: {
+ notesApp,
+ },
+ store,
+ data() {
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
+
+ return {
+ noteableData,
+ currentUserData: JSON.parse(notesDataset.currentUserData),
+ notesData: JSON.parse(notesDataset.notesData),
+ helpPagePath: notesDataset.helpPagePath,
+ };
+ },
+ computed: {
+ ...mapGetters(['discussionTabCounter']),
+ ...mapState({
+ activeTab: state => state.page.activeTab,
+ }),
+ },
+ watch: {
+ discussionTabCounter() {
+ this.updateDiscussionTabCounter();
+ },
+ },
+ created() {
+ this.setActiveTab(window.mrTabs.getCurrentAction());
+ },
+ mounted() {
+ this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
+ $(document).on('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
+ },
+ beforeDestroy() {
+ $(document).off('visibilitychange', this.updateDiscussionTabCounter);
+ window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
+ },
+ methods: {
+ ...mapActions(['setActiveTab']),
+ updateDiscussionTabCounter() {
+ this.notesCountBadge.text(this.discussionTabCounter);
+ },
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ shouldShow: this.activeTab === 'show',
+ helpPagePath: this.helpPagePath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index bd6736152f5..eefc801ed7a 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -1,11 +1,12 @@
<script>
-import CodeCell from './code/index.vue';
+import CodeOutput from './code/index.vue';
import OutputCell from './output/index.vue';
export default {
+ name: 'CodeCell',
components: {
- 'code-cell': CodeCell,
- 'output-cell': OutputCell,
+ CodeOutput,
+ OutputCell,
},
props: {
cell: {
@@ -29,8 +30,8 @@ export default {
hasOutput() {
return this.cell.outputs.length;
},
- output() {
- return this.cell.outputs[0];
+ outputs() {
+ return this.cell.outputs;
},
},
};
@@ -38,7 +39,7 @@ export default {
<template>
<div class="cell">
- <code-cell
+ <code-output
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass"
@@ -47,7 +48,7 @@ export default {
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
- :output="output"
+ :outputs="outputs"
:code-css-class="codeCssClass"
/>
</div>
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index 8bf2431c4c6..98b6cdd0944 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -3,8 +3,9 @@ import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
+ name: 'CodeOutput',
components: {
- prompt: Prompt,
+ Prompt,
},
props: {
count: {
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index c6fc786fa76..8dc2d73af9b 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -4,13 +4,21 @@ import Prompt from '../prompt.vue';
export default {
components: {
- prompt: Prompt,
+ Prompt,
},
props: {
+ count: {
+ type: Number,
+ required: true,
+ },
rawCode: {
type: String,
required: true,
},
+ index: {
+ type: Number,
+ required: true,
+ },
},
computed: {
sanitizedOutput() {
@@ -21,13 +29,16 @@ export default {
},
});
},
+ showOutput() {
+ return this.index === 0;
+ },
},
};
</script>
<template>
<div class="output">
- <prompt />
+ <prompt type="Out" :count="count" :show-output="showOutput" />
<div v-html="sanitizedOutput"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue
index fe8c81398fb..f1130275525 100644
--- a/app/assets/javascripts/notebook/cells/output/image.vue
+++ b/app/assets/javascripts/notebook/cells/output/image.vue
@@ -6,6 +6,10 @@ export default {
prompt: Prompt,
},
props: {
+ count: {
+ type: Number,
+ required: true,
+ },
outputType: {
type: String,
required: true,
@@ -14,10 +18,24 @@ export default {
type: String,
required: true,
},
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ imgSrc() {
+ return `data:${this.outputType};base64,${this.rawCode}`;
+ },
+ showOutput() {
+ return this.index === 0;
+ },
},
};
</script>
<template>
- <div class="output"><prompt /> <img :src="'data:' + outputType + ';base64,' + rawCode" /></div>
+ <div class="output">
+ <prompt type="out" :count="count" :show-output="showOutput" /> <img :src="imgSrc" />
+ </div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index bd0bcc0d819..c5ae7e7ee10 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -1,14 +1,9 @@
<script>
-import CodeCell from '../code/index.vue';
-import Html from './html.vue';
-import Image from './image.vue';
+import CodeOutput from '../code/index.vue';
+import HtmlOutput from './html.vue';
+import ImageOutput from './image.vue';
export default {
- components: {
- 'code-cell': CodeCell,
- 'html-output': Html,
- 'image-output': Image,
- },
props: {
codeCssClass: {
type: String,
@@ -20,68 +15,69 @@ export default {
required: false,
default: 0,
},
- output: {
- type: Object,
+ outputs: {
+ type: Array,
required: true,
- default: () => ({}),
},
},
- computed: {
- componentName() {
- if (this.output.text) {
- return 'code-cell';
- } else if (this.output.data['image/png']) {
- return 'image-output';
- } else if (this.output.data['text/html']) {
- return 'html-output';
- } else if (this.output.data['image/svg+xml']) {
- return 'html-output';
- }
+ data() {
+ return {
+ outputType: '',
+ };
+ },
+ methods: {
+ dataForType(output, type) {
+ let data = output.data[type];
- return 'code-cell';
- },
- rawCode() {
- if (this.output.text) {
- return this.output.text.join('');
+ if (typeof data === 'object') {
+ data = data.join('');
}
- return this.dataForType(this.outputType);
+ return data;
},
- outputType() {
- if (this.output.text) {
- return '';
- } else if (this.output.data['image/png']) {
- return 'image/png';
- } else if (this.output.data['text/html']) {
- return 'text/html';
- } else if (this.output.data['image/svg+xml']) {
- return 'image/svg+xml';
+ getComponent(output) {
+ if (output.text) {
+ return CodeOutput;
+ } else if (output.data['image/png']) {
+ this.outputType = 'image/png';
+
+ return ImageOutput;
+ } else if (output.data['text/html']) {
+ this.outputType = 'text/html';
+
+ return HtmlOutput;
+ } else if (output.data['image/svg+xml']) {
+ this.outputType = 'image/svg+xml';
+
+ return HtmlOutput;
}
- return 'text/plain';
+ this.outputType = 'text/plain';
+ return CodeOutput;
},
- },
- methods: {
- dataForType(type) {
- let data = this.output.data[type];
-
- if (typeof data === 'object') {
- data = data.join('');
+ rawCode(output) {
+ if (output.text) {
+ return output.text.join('');
}
- return data;
+ return this.dataForType(output, this.outputType);
},
},
};
</script>
<template>
- <component
- :is="componentName"
- :output-type="outputType"
- :count="count"
- :raw-code="rawCode"
- :code-css-class="codeCssClass"
- type="output"
- />
+ <div>
+ <component
+ :is="getComponent(output)"
+ v-for="(output, index) in outputs"
+ :key="index"
+ type="output"
+ :output-type="outputType"
+ :count="count"
+ :index="index"
+ :raw-code="rawCode(output)"
+ :code-css-class="codeCssClass"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue
index 3f1f239a806..1eeb61844a4 100644
--- a/app/assets/javascripts/notebook/cells/prompt.vue
+++ b/app/assets/javascripts/notebook/cells/prompt.vue
@@ -11,18 +11,26 @@ export default {
required: false,
default: 0,
},
+ showOutput: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
hasKeys() {
return this.type !== '' && this.count;
},
+ showTypeText() {
+ return this.type && this.count && this.showOutput;
+ },
},
};
</script>
<template>
<div class="prompt">
- <span v-if="hasKeys"> {{ type }} [{{ count }}]: </span>
+ <span v-if="showTypeText"> {{ type }} [{{ count }}]: </span>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index 6a54d0b3823..e7056c03e4a 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells';
export default {
components: {
- 'code-cell': CodeCell,
- 'markdown-cell': MarkdownCell,
+ CodeCell,
+ MarkdownCell,
},
props: {
notebook: {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index dfb53c986fc..c9c01354333 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -138,8 +138,6 @@ export default class Notes {
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
- // reset main target form when clicking discard
- this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
@@ -191,7 +189,6 @@ export default class Notes {
this.$wrapperEl.off('keyup input', '.js-note-text');
this.$wrapperEl.off('click', '.js-note-target-reopen');
this.$wrapperEl.off('click', '.js-note-target-close');
- this.$wrapperEl.off('click', '.js-note-discard');
this.$wrapperEl.off('keydown', '.js-note-text');
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
@@ -986,11 +983,9 @@ export default class Notes {
form.find('#note_position').val(dataHolder.attr('data-position'));
form
- .find('.js-note-discard')
+ .find('.js-close-discussion-note-form')
.show()
- .removeClass('js-note-discard')
- .addClass('js-close-discussion-note-form')
- .text(form.find('.js-close-discussion-note-form').data('cancelText'));
+ .removeClass('hide');
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
@@ -1194,12 +1189,11 @@ export default class Notes {
}
updateTargetButtons(e) {
- var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea;
+ var closebtn, closetext, form, reopenbtn, reopentext, textarea;
textarea = $(e.target);
form = textarea.parents('form');
reopenbtn = form.find('.js-note-target-reopen');
closebtn = form.find('.js-note-target-close');
- discardbtn = form.find('.js-note-discard');
if (textarea.val().trim().length > 0) {
reopentext = reopenbtn.attr('data-alternative-text');
@@ -1216,9 +1210,6 @@ export default class Notes {
if (closebtn.is(':not(.btn-comment-and-close)')) {
closebtn.addClass('btn-comment-and-close');
}
- if (discardbtn.is(':hidden')) {
- return discardbtn.show();
- }
} else {
reopentext = reopenbtn.data('originalText');
closetext = closebtn.data('originalText');
@@ -1234,9 +1225,6 @@ export default class Notes {
if (closebtn.is('.btn-comment-and-close')) {
closebtn.removeClass('btn-comment-and-close');
}
- if (discardbtn.is(':visible')) {
- return discardbtn.hide();
- }
}
}
@@ -1251,15 +1239,13 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType');
- var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
- .attr('data-remote', 'true')
- .attr('data-markdown-version', markdownVersion);
+ .attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index ce56beb1e6b..1d6cb9485f7 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -39,11 +39,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
},
data() {
return {
@@ -342,7 +337,6 @@ Please check your network connection and try again.`;
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
- :markdown-version="markdownVersion"
:add-spacing-classes="false"
>
<textarea
@@ -357,9 +351,9 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
data-supports-quick-actions="true"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
- @keydown.up="editCurrentUserLastNote();"
- @keydown.meta.enter="handleSave();"
- @keydown.ctrl.enter="handleSave();"
+ @keydown.up="editCurrentUserLastNote()"
+ @keydown.meta.enter="handleSave()"
+ @keydown.ctrl.enter="handleSave()"
>
</textarea>
</markdown-field>
@@ -370,10 +364,10 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<button
:disabled="isSubmitButtonDisabled"
- class="btn btn-create comment-btn js-comment-button js-comment-submit-button
+ class="btn btn-success js-comment-button js-comment-submit-button
qa-comment-button"
type="submit"
- @click.prevent="handleSave();"
+ @click.prevent="handleSave()"
>
{{ __(commentButtonTitle) }}
</button>
@@ -381,7 +375,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown"
@@ -394,7 +388,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<button
type="button"
class="btn btn-transparent"
- @click.prevent="setNoteType('comment');"
+ @click.prevent="setNoteType('comment')"
>
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
@@ -408,7 +402,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<button
type="button"
class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion');"
+ @click.prevent="setNoteType('discussion')"
>
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
@@ -429,17 +423,8 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
- @click="handleSave(true);"
+ @click="handleSave(true)"
/>
-
- <button
- v-if="note.length"
- type="button"
- class="btn btn-cancel js-note-discard"
- @click="discard"
- >
- Discard draft
- </button>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index af821df0fd2..d8947e8ca50 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -5,8 +5,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
import { getDiffMode } from '~/diffs/store/utils';
-
-const FIRST_CHAR_REGEX = /^(\+|-| )/;
+import { diffViewerModes } from '~/ide/constants';
export default {
components: {
@@ -33,6 +32,12 @@ export default {
diffMode() {
return getDiffMode(this.discussion.diff_file);
},
+ diffViewerMode() {
+ return this.discussion.diff_file.viewer.name;
+ },
+ isTextFile() {
+ return this.diffViewerMode === diffViewerModes.text;
+ },
hasTruncatedDiffLines() {
return (
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
@@ -54,27 +59,20 @@ export default {
this.error = true;
});
},
- trimChar(line) {
- return line.replace(FIRST_CHAR_REGEX, '');
- },
},
userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
<template>
- <div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder">
+ <div :class="{ 'text-file': isTextFile }" class="diff-file file-holder">
<diff-file-header
:discussion-path="discussion.discussion_path"
:diff-file="discussion.diff_file"
:can-current-user-fork="false"
- :expanded="!discussion.diff_file.collapsed"
+ :expanded="!discussion.diff_file.viewer.collapsed"
/>
- <div
- v-if="discussion.diff_file.text"
- :class="$options.userColorSchemeClass"
- class="diff-content code"
- >
+ <div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code">
<table>
<template v-if="hasTruncatedDiffLines">
<tr
@@ -85,7 +83,7 @@ export default {
>
<td class="diff-line-num old_line">{{ line.old_line }}</td>
<td class="diff-line-num new_line">{{ line.new_line }}</td>
- <td :class="line.type" class="line_content" v-html="trimChar(line.rich_text)"></td>
+ <td :class="line.type" class="line_content" v-html="line.rich_text"></td>
</tr>
</template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">
@@ -114,6 +112,7 @@ export default {
<div v-else>
<diff-viewer
:diff-mode="diffMode"
+ :diff-viewer-mode="diffViewerMode"
:new-path="discussion.diff_file.new_path"
:new-sha="discussion.diff_file.diff_refs.head_sha"
:old-path="discussion.diff_file.old_path"
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 86c114a761a..31164f74201 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,8 +1,15 @@
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
+import { getLocationHash } from '../../lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
+import {
+ DISCUSSION_FILTERS_DEFAULT_VALUE,
+ HISTORY_ONLY_FILTER_VALUE,
+ DISCUSSION_TAB_LABEL,
+ DISCUSSION_FILTER_TYPES,
+} from '../constants';
+import notesEventHub from '../event_hub';
export default {
components: {
@@ -23,6 +30,7 @@ export default {
return {
currentValue: this.selectedValue,
defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ displayFilters: true,
};
},
computed: {
@@ -32,31 +40,73 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue);
},
},
+ created() {
+ if (window.mrTabs) {
+ const { eventHub, currentTab } = window.mrTabs;
+
+ eventHub.$on('MergeRequestTabChange', this.toggleFilters);
+ this.toggleFilters(currentTab);
+ }
+
+ notesEventHub.$on('dropdownSelect', this.selectFilter);
+ window.addEventListener('hashchange', this.handleLocationHash);
+ this.handleLocationHash();
+ },
mounted() {
this.toggleCommentsForm();
},
+ destroyed() {
+ notesEventHub.$off('dropdownSelect', this.selectFilter);
+ window.removeEventListener('hashchange', this.handleLocationHash);
+ },
methods: {
- ...mapActions(['filterDiscussion', 'setCommentsDisabled']),
+ ...mapActions(['filterDiscussion', 'setCommentsDisabled', 'setTargetNoteHash']),
selectFilter(value) {
const filter = parseInt(value, 10);
// close dropdown
- $(this.$refs.dropdownToggle).dropdown('toggle');
+ this.toggleDropdown();
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
this.toggleCommentsForm();
},
+ toggleDropdown() {
+ $(this.$refs.dropdownToggle).dropdown('toggle');
+ },
toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
+ toggleFilters(tab) {
+ this.displayFilters = tab === DISCUSSION_TAB_LABEL;
+ },
+ handleLocationHash() {
+ const hash = getLocationHash();
+
+ if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) {
+ this.selectFilter(this.defaultValue);
+ this.toggleDropdown(); // close dropdown
+ this.setTargetNoteHash(hash);
+ }
+ },
+ filterType(value) {
+ if (value === 0) {
+ return DISCUSSION_FILTER_TYPES.ALL;
+ } else if (value === 1) {
+ return DISCUSSION_FILTER_TYPES.COMMENTS;
+ }
+ return DISCUSSION_FILTER_TYPES.HISTORY;
+ },
},
};
</script>
<template>
- <div class="discussion-filter-container d-inline-block align-bottom">
+ <div
+ v-if="displayFilters"
+ class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom"
+ >
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
@@ -67,17 +117,22 @@ export default {
{{ currentFilter.title }} <icon name="chevron-down" />
</button>
<div
+ ref="dropdownMenu"
class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
aria-labelledby="discussion-filter-dropdown"
>
<div class="dropdown-content">
<ul>
- <li v-for="filter in filters" :key="filter.value">
+ <li
+ v-for="filter in filters"
+ :key="filter.value"
+ :data-filter-type="filterType(filter.value)"
+ >
<button
:class="{ 'is-active': filter.value === currentValue }"
class="qa-filter-options"
type="button"
- @click="selectFilter(filter.value);"
+ @click="selectFilter(filter.value)"
>
{{ filter.title }}
</button>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
new file mode 100644
index 00000000000..46661e06f6d
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
+
+import notesEventHub from '../event_hub';
+
+export default {
+ components: {
+ GlButton,
+ Icon,
+ },
+ computed: {
+ timelineContent() {
+ return sprintf(
+ __(
+ "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options.",
+ ),
+ {
+ startTag: `<b>`,
+ endTag: `</b>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ selectFilter(value) {
+ notesEventHub.$emit('dropdownSelect', value);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note">
+ <div class="timeline-icon">
+ <icon name="comment" />
+ </div>
+ <div class="timeline-content">
+ <div v-html="timelineContent"></div>
+ <div class="discussion-filter-actions mt-2">
+ <gl-button variant="default" @click="selectFilter(0)">
+ {{ __('Show all activity') }}
+ </gl-button>
+ <gl-button variant="default" @click="selectFilter(1)">
+ {{ __('Show comments only') }}
+ </gl-button>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
new file mode 100644
index 00000000000..07a5bda6bcb
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -0,0 +1,28 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ name: 'JumpToNextDiscussionButton',
+ components: {
+ icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+};
+</script>
+
+<template>
+ <div class="btn-group" role="group">
+ <button
+ ref="button"
+ v-gl-tooltip
+ class="btn btn-default discussion-next-btn"
+ :title="s__('MergeRequests|Jump to next unresolved discussion')"
+ @click="$emit('onClick')"
+ >
+ <icon name="comment-next" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
new file mode 100644
index 00000000000..ea590905e3c
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ name: 'ReplyPlaceholder',
+};
+</script>
+
+<template>
+ <button
+ ref="button"
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field"
+ :title="s__('MergeRequests|Add a reply')"
+ @click="$emit('onClick')"
+ >
+ {{ s__('MergeRequests|Reply...') }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
new file mode 100644
index 00000000000..2b29d710236
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -0,0 +1,28 @@
+<script>
+export default {
+ name: 'ResolveDiscussionButton',
+ props: {
+ isResolving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ buttonTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
+ <i
+ v-if="isResolving"
+ ref="isResolvingIcon"
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin"
+ ></i>
+ {{ buttonTitle }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
new file mode 100644
index 00000000000..e413398696a
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -0,0 +1,34 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+
+export default {
+ name: 'ResolveWithIssueButton',
+ components: {
+ Icon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="btn-group" role="group">
+ <gl-button
+ v-gl-tooltip
+ :href="url"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
+ class="new-issue-for-discussion discussion-create-issue-btn"
+ >
+ <icon name="issue-new" />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index d99694b06e9..de1ea0f58d6 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -2,11 +2,13 @@
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import ReplyButton from './note_actions/reply_button.vue';
export default {
name: 'NoteActions',
components: {
Icon,
+ ReplyButton,
GlLoadingIcon,
},
directives: {
@@ -36,6 +38,10 @@ export default {
required: false,
default: null,
},
+ showReply: {
+ type: Boolean,
+ required: true,
+ },
canEdit: {
type: Boolean,
required: true,
@@ -80,6 +86,9 @@ export default {
},
computed: {
...mapGetters(['getUserDataByProp']),
+ showReplyButton() {
+ return gon.features && gon.features.replyToIndividualNotes && this.showReply;
+ },
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
@@ -112,6 +121,11 @@ export default {
onResolve() {
this.$emit('handleResolve');
},
+ closeTooltip() {
+ this.$nextTick(() => {
+ this.$root.$emit('bv::hide::tooltip');
+ });
+ },
},
};
</script>
@@ -121,6 +135,7 @@ export default {
<span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
<div v-if="canResolve" class="note-actions-item">
<button
+ ref="resolveButton"
v-gl-tooltip
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
@@ -137,10 +152,9 @@ export default {
</div>
<div v-if="canAwardEmoji" class="note-actions-item">
<a
- v-gl-tooltip.bottom
+ v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button js-add-award js-note-emoji"
- data-position="right"
href="#"
title="Add reaction"
>
@@ -153,9 +167,15 @@ export default {
<icon css-classes="link-highlight award-control-icon-super-positive" name="emoji_smiley" />
</a>
</div>
+ <reply-button
+ v-if="showReplyButton"
+ ref="replyButton"
+ class="js-reply-button"
+ @startReplying="$emit('startReplying')"
+ />
<div v-if="canEdit" class="note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
@@ -166,7 +186,7 @@ export default {
</div>
<div v-if="showDeleteAction" class="note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
@@ -177,11 +197,12 @@ export default {
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
<button
- v-gl-tooltip.bottom
+ v-gl-tooltip
type="button"
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
data-toggle="dropdown"
+ @click="closeTooltip"
>
<icon css-classes="icon" name="ellipsis_v" />
</button>
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
new file mode 100644
index 00000000000..f50cab81efe
--- /dev/null
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'ReplyButton',
+ components: {
+ Icon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+};
+</script>
+
+<template>
+ <div class="note-actions-item">
+ <gl-button
+ ref="button"
+ v-gl-tooltip.bottom
+ class="note-action-button"
+ variant="transparent"
+ :title="__('Reply to comment')"
+ @click="$emit('startReplying')"
+ >
+ <icon name="comment" css-classes="link-highlight" />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 3d60eb02db8..17e5fcab5b7 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlTooltipDirective } from '@gitlab/ui';
+import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
@@ -10,7 +10,7 @@ export default {
Icon,
},
directives: {
- GlTooltip: GlTooltipDirective,
+ tooltip,
},
props: {
awards: {
@@ -167,25 +167,25 @@ export default {
<button
v-for="(awardList, awardName, index) in groupedAwards"
:key="index"
- v-gl-tooltip.bottom="{ boundary: 'viewport' }"
+ v-tooltip
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
+ data-boundary="viewport"
class="btn award-control"
type="button"
- @click="handleAward(awardName);"
+ @click="handleAward(awardName)"
>
<span v-html="getAwardHTML(awardName)"></span>
<span class="award-control-text js-counter">{{ awardList.length }}</span>
</button>
<div v-if="canAwardEmoji" class="award-menu-holder">
<button
- v-gl-tooltip
+ v-tooltip
:class="{ 'js-user-authored': isAuthoredByMe }"
class="award-control btn js-add-award"
title="Add reaction"
aria-label="Add reaction"
data-boundary="viewport"
- data-placement="bottom"
type="button"
>
<span class="award-control-icon award-control-icon-neutral">
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index bcf5d334da4..fb1d98355b3 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -95,6 +95,7 @@ export default {
<div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body">
<suggestions
v-if="hasSuggestion && !isEditing"
+ class="note-text md"
:suggestions="note.suggestions"
:note-html="note.note_html"
:line-type="lineType"
@@ -111,7 +112,6 @@ export default {
:line="line"
:note="note"
:help-page-path="helpPagePath"
- :markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 9b7f3d3588d..92258a25438 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -6,6 +6,7 @@ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
+import { __ } from '~/locale';
export default {
name: 'NoteForm',
@@ -25,15 +26,10 @@ export default {
required: false,
default: '',
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
saveButtonTitle: {
type: String,
required: false,
- default: 'Save comment',
+ default: __('Save comment'),
},
discussion: {
type: Object,
@@ -149,6 +145,9 @@ export default {
return shouldResolve || shouldToggleState;
},
+ handleKeySubmit() {
+ this.handleUpdate();
+ },
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
@@ -198,7 +197,6 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
- :markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
:line="line"
:note="discussionNote"
@@ -216,32 +214,32 @@ export default {
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
- @keydown.meta.enter="handleUpdate();"
- @keydown.ctrl.enter="handleUpdate();"
- @keydown.up="editMyLastNote();"
- @keydown.esc="cancelHandler(true);"
+ @keydown.meta.enter="handleKeySubmit()"
+ @keydown.ctrl.enter="handleKeySubmit()"
+ @keydown.up="editMyLastNote()"
+ @keydown.esc="cancelHandler(true)"
></textarea>
</markdown-field>
<div class="note-form-actions clearfix">
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-success js-comment-button"
- @click="handleUpdate();"
+ class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button"
+ @click="handleUpdate()"
>
{{ saveButtonTitle }}
</button>
<button
v-if="discussion.resolvable"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
- @click.prevent="handleUpdate(true);"
+ @click.prevent="handleUpdate(true)"
>
{{ resolveButtonTitle }}
</button>
<button
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler();"
+ @click="cancelHandler()"
>
Cancel
</button>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 7b39901024d..68b753a4abf 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -69,7 +69,7 @@ export default {
type="button"
@click="handleToggle"
>
- <i :class="toggleChevronClass" class="fa" aria-hidden="true"> </i>
+ <i :class="toggleChevronClass" class="fa" aria-hidden="true"></i>
{{ __('Toggle discussion') }}
</button>
</div>
@@ -81,19 +81,18 @@ export default {
:data-user-id="author.id"
:data-username="author.username"
>
+ <slot name="note-header-info"></slot>
<span class="note-header-author-name">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
- <span class="note-headline-light"> @{{ author.username }} </span>
+ <span class="note-headline-light">@{{ author.username }}</span>
</a>
- <span v-else> {{ __('A deleted user') }} </span>
+ <span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light">
<span class="note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
<template v-if="createdAt">
<span class="system-note-separator">
- <template v-if="actionText">
- {{ actionText }}
- </template>
+ <template v-if="actionText">{{ actionText }}</template>
</span>
<a
:href="noteTimestampLink"
@@ -107,8 +106,7 @@ export default {
class="fa fa-spinner fa-spin editing-spinner"
aria-label="Comment is being updated"
aria-hidden="true"
- >
- </i>
+ ></i>
</span>
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 7c3f5d00308..fc51998935d 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -6,12 +6,14 @@ import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
+import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
+import resolveDiscussionButton from './discussion_resolve_button.vue';
import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
@@ -23,6 +25,10 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
+import ReplyPlaceholder from './discussion_reply_placeholder.vue';
+import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
+import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
+import eventHub from '../event_hub';
export default {
name: 'NoteableDiscussion',
@@ -34,16 +40,21 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ resolveDiscussionButton,
+ jumpToNextDiscussionButton,
toggleRepliesWidget,
+ ReplyPlaceholder,
placeholderNote,
placeholderSystemNote,
+ ResolveWithIssueButton,
systemNote,
+ DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
TimelineEntryItem,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [autosave, noteable, resolvable, discussionNavigation],
+ mixins: [autosave, noteable, resolvable, discussionNavigation, diffLineNoteFormMixin],
props: {
discussion: {
type: Object,
@@ -87,6 +98,7 @@ export default {
},
computed: {
...mapGetters([
+ 'convertedDisscussionIds',
'getNoteableData',
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
@@ -206,11 +218,28 @@ export default {
return sprintf(text, { commitId, linkStart, linkEnd }, false);
},
diffLine() {
+ if (this.line) {
+ return this.line;
+ }
+
if (this.discussion.diff_discussion && this.discussion.truncated_diff_lines) {
return this.discussion.truncated_diff_lines.slice(-1)[0];
}
- return this.line;
+ return null;
+ },
+ commit() {
+ if (!this.discussion.for_commit) {
+ return null;
+ }
+
+ return {
+ id: this.discussion.commit_id,
+ url: this.discussion.discussion_path,
+ };
+ },
+ resolveWithIssuePath() {
+ return !this.discussionResolved && this.discussion.resolve_with_issue_path;
},
},
watch: {
@@ -225,6 +254,12 @@ export default {
}
},
},
+ created() {
+ eventHub.$on('startReplying', this.onStartReplying);
+ },
+ beforeDestroy() {
+ eventHub.$off('startReplying', this.onStartReplying);
+ },
methods: {
...mapActions([
'saveNote',
@@ -232,6 +267,7 @@ export default {
'removePlaceholderNotes',
'toggleResolveNote',
'expandDiscussion',
+ 'removeConvertedDiscussion',
]),
truncateSha,
componentName(note) {
@@ -271,6 +307,10 @@ export default {
}
}
+ if (this.convertedDisscussionIds.includes(this.discussion.id)) {
+ this.removeConvertedDiscussion(this.discussion.id);
+ }
+
this.isReplying = false;
this.resetAutoSave();
},
@@ -281,6 +321,10 @@ export default {
note: { note: noteText },
};
+ if (this.convertedDisscussionIds.includes(this.discussion.id)) {
+ postData.return_discussion = true;
+ }
+
if (this.discussion.for_commit) {
postData.note_project_id = this.discussion.project_id;
}
@@ -320,6 +364,11 @@ Please check your network connection and try again.`;
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
},
+ onStartReplying(discussionId) {
+ if (this.discussion.id === discussionId) {
+ this.showReplyForm();
+ }
+ },
},
};
</script>
@@ -338,30 +387,32 @@ Please check your network connection and try again.`;
:img-size="40"
/>
</div>
- <note-header
- :author="author"
- :created-at="initialDiscussion.created_at"
- :note-id="initialDiscussion.id"
- :include-toggle="true"
- :expanded="discussion.expanded"
- @toggleHandler="toggleDiscussionHandler"
- >
- <span v-html="actionText"></span>
- </note-header>
- <note-edited-text
- v-if="discussion.resolved"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline"
- />
- <note-edited-text
- v-else-if="lastUpdatedAt"
- :edited-at="lastUpdatedAt"
- :edited-by="lastUpdatedBy"
- action-text="Last updated"
- class-name="discussion-headline-light js-discussion-headline"
- />
+ <div class="timeline-content">
+ <note-header
+ :author="author"
+ :created-at="initialDiscussion.created_at"
+ :note-id="initialDiscussion.id"
+ :include-toggle="true"
+ :expanded="discussion.expanded"
+ @toggleHandler="toggleDiscussionHandler"
+ >
+ <span v-html="actionText"></span>
+ </note-header>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ <note-edited-text
+ v-else-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ action-text="Last updated"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
</div>
<div v-if="shouldShowDiscussions" class="discussion-body">
<component
@@ -376,8 +427,11 @@ Please check your network connection and try again.`;
:is="componentName(initialDiscussion)"
:note="componentData(initialDiscussion)"
:line="line"
+ :commit="commit"
:help-page-path="helpPagePath"
+ :show-reply-button="canReply"
@handleDeleteNote="deleteNoteHandler"
+ @startReplying="showReplyForm"
>
<note-edited-text
v-if="discussion.resolved"
@@ -428,49 +482,26 @@ Please check your network connection and try again.`;
>
<template v-if="!isReplying && canReply">
<div class="discussion-with-resolve-btn">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm"
- >
- Reply...
- </button>
- <div v-if="discussion.resolvable">
- <button
- type="button"
- class="btn btn-default ml-sm-2"
- @click="resolveHandler();"
- >
- <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
- {{ resolveButtonTitle }}
- </button>
- </div>
+ <reply-placeholder class="qa-discussion-reply" @onClick="showReplyForm" />
+ <resolve-discussion-button
+ v-if="discussion.resolvable"
+ :is-resolving="isResolving"
+ :button-title="resolveButtonTitle"
+ @onClick="resolveHandler"
+ />
<div
v-if="discussion.resolvable"
class="btn-group discussion-actions ml-sm-2"
role="group"
>
- <div v-if="!discussionResolved" class="btn-group" role="group">
- <a
- v-gl-tooltip
- :href="discussion.resolve_with_issue_path"
- :title="s__('MergeRequests|Resolve this discussion in a new issue')"
- class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
- >
- <icon name="issue-new" />
- </a>
- </div>
- <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group">
- <button
- v-gl-tooltip
- class="btn btn-default discussion-next-btn"
- title="Jump to next unresolved discussion"
- @click="jumpToNextDiscussion"
- >
- <icon name="comment-next" />
- </button>
- </div>
+ <resolve-with-issue-button
+ v-if="resolveWithIssuePath"
+ :url="resolveWithIssuePath"
+ />
+ <jump-to-next-discussion-button
+ v-if="shouldShowJumpToNextDiscussion"
+ @onClick="jumpToNextDiscussion"
+ />
</div>
</div>
</template>
@@ -481,6 +512,7 @@ Please check your network connection and try again.`;
:is-editing="false"
:line="diffLine"
save-button-title="Comment"
+ @handleFormUpdateAddToReview="addReplyToReview"
@handleFormUpdate="saveReply"
@cancelForm="cancelReplyForm"
/>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 4c02588127e..5fa0ab3de98 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,7 +2,9 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import { s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -37,6 +39,16 @@ export default {
required: false,
default: '',
},
+ commit: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
+ showReplyButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -47,7 +59,7 @@ export default {
};
},
computed: {
- ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData']),
+ ...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']),
author() {
return this.note.author;
},
@@ -73,6 +85,27 @@ export default {
isTarget() {
return this.targetNoteHash === this.noteAnchorId;
},
+ discussionId() {
+ if (this.discussion) {
+ return this.discussion.id;
+ }
+ return '';
+ },
+ actionText() {
+ if (!this.commit) {
+ return '';
+ }
+
+ // We need to do this to ensure we have the currect sentence order
+ // when translating this as the sentence order may change from one
+ // language to the next. See:
+ // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771
+ const { id, url } = this.commit;
+ const commitLink = `<a class="commit-sha monospace" href="${escape(url)}">${truncateSha(
+ id,
+ )}</a>`;
+ return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false);
+ },
},
created() {
@@ -186,7 +219,7 @@ export default {
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note note-wrapper"
+ class="note note-wrapper qa-noteable-note-item"
>
<div v-once class="timeline-icon">
<user-avatar-link
@@ -200,18 +233,16 @@ export default {
</div>
<div class="timeline-content">
<div class="note-header">
- <note-header
- v-once
- :author="author"
- :created-at="note.created_at"
- :note-id="note.id"
- action-text="commented"
- />
+ <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
+ <span v-if="commit" v-html="actionText"></span>
+ <span v-else class="d-none d-sm-inline">&middot;</span>
+ </note-header>
<note-actions
:author-id="author.id"
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
+ :show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
@@ -225,6 +256,7 @@ export default {
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
+ @startReplying="$emit('startReplying')"
/>
</div>
<div class="timeline-discussion-body">
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index f3fcfdfda05..e2bd59f7631 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -6,6 +6,7 @@ import * as constants from '../constants';
import eventHub from '../event_hub';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
+import discussionFilterNote from './discussion_filter_note.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
@@ -24,6 +25,7 @@ export default {
placeholderNote,
placeholderSystemNote,
skeletonLoadingContainer,
+ discussionFilterNote,
},
props: {
noteableData: {
@@ -44,11 +46,6 @@ export default {
required: false,
default: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
helpPagePath: {
type: String,
required: false,
@@ -65,9 +62,11 @@ export default {
...mapGetters([
'isNotesFetched',
'discussions',
+ 'convertedDisscussionIds',
'getNotesDataByProp',
'isLoading',
'commentsDisabled',
+ 'getNoteableData',
]),
noteableType() {
return this.noteableData.noteableType;
@@ -83,6 +82,9 @@ export default {
return this.discussions;
},
+ canReply() {
+ return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled;
+ },
},
watch: {
shouldShow() {
@@ -90,8 +92,15 @@ export default {
this.fetchNotes();
}
},
+ allDiscussions() {
+ if (this.discussonsCount) {
+ this.discussonsCount.textContent = this.allDiscussions.length;
+ }
+ },
},
created() {
+ this.discussonsCount = document.querySelector('.js-discussions-count');
+
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
@@ -133,6 +142,7 @@ export default {
'setNotesFetchedState',
'expandDiscussion',
'startTaskList',
+ 'convertToDiscussion',
]),
fetchNotes() {
if (this.isFetching) return null;
@@ -180,6 +190,11 @@ export default {
}
}
},
+ startReplying(discussionId) {
+ return this.convertToDiscussion(discussionId)
+ .then(() => this.$nextTick())
+ .then(() => eventHub.$emit('startReplying', discussionId));
+ },
},
systemNote: constants.SYSTEM_NOTE,
};
@@ -198,13 +213,21 @@ export default {
/>
<placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" />
</template>
- <template v-else-if="discussion.individual_note">
+ <template
+ v-else-if="discussion.individual_note && !convertedDisscussionIds.includes(discussion.id)"
+ >
<system-note
v-if="discussion.notes[0].system"
:key="discussion.id"
:note="discussion.notes[0]"
/>
- <noteable-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ <noteable-note
+ v-else
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ :show-reply-button="canReply"
+ @startReplying="startReplying(discussion.id)"
+ />
</template>
<noteable-discussion
v-else
@@ -214,12 +237,9 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
+ <discussion-filter-note v-show="commentsDisabled" />
</ul>
- <comment-form
- v-if="!commentsDisabled"
- :noteable-type="noteableType"
- :markdown-version="markdownVersion"
- />
+ <comment-form v-if="!commentsDisabled" :noteable-type="noteableType" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 72a8ff28466..f1b0b12bdce 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -57,7 +57,7 @@ export default {
tooltip-placement="bottom"
/>
</div>
- <button class="btn btn-link js-replies-text" type="button" @click="toggle">
+ <button class="btn btn-link js-replies-text qa-expand-replies" type="button" @click="toggle">
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button>
{{ __('Last reply by') }}
@@ -66,7 +66,11 @@ export default {
</a>
<time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
</template>
- <span v-else class="collapse-replies-btn js-collapse-replies" @click="toggle">
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies qa-collapse-replies"
+ @click="toggle"
+ >
<icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
</span>
</li>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 3147dc64c27..fba3db8542c 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -17,9 +17,16 @@ export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
+export const DISCUSSION_TAB_LABEL = 'show';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
Epic: EPIC_NOTEABLE_TYPE,
};
+
+export const DISCUSSION_FILTER_TYPES = {
+ ALL: 'all',
+ COMMENTS: 'comments',
+ HISTORY: 'history',
+};
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 2f715c85fa6..4883266dae5 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -18,7 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
- const markdownVersion = parseInt(notesDataset.markdownVersion, 10);
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
@@ -37,7 +36,6 @@ document.addEventListener('DOMContentLoaded', () => {
return {
noteableData,
currentUserData,
- markdownVersion,
notesData: JSON.parse(notesDataset.notesData),
};
},
@@ -47,7 +45,6 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
- markdownVersion: this.markdownVersion,
},
});
},
diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
new file mode 100644
index 00000000000..188556e8921
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
@@ -0,0 +1,10 @@
+export default {
+ computed: {
+ draftForDiscussion: () => () => ({}),
+ },
+ methods: {
+ showDraft: () => false,
+ addReplyToReview: () => {},
+ addToReview: () => {},
+ },
+};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 8edf3d088bb..2329727bca2 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -31,6 +31,10 @@ export default {
},
methods: {
resolveHandler(resolvedState = false) {
+ if (this.note && this.note.isDraft) {
+ return this.$emit('toggleResolveStatus');
+ }
+
this.isResolving = true;
const isResolved = this.discussionResolved || resolvedState;
const discussion = this.resolveAsThread;
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 65f85314fa0..1a0dba69a7c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -83,12 +83,44 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
dispatch('startTaskList');
});
-export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
+export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
+ const { notesById } = getters;
+
+ notes.forEach(note => {
+ if (notesById[note.id]) {
+ commit(types.UPDATE_NOTE, note);
+ } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
+ const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
+
+ if (discussion) {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else if (note.type === constants.DIFF_NOTE) {
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ });
+};
+
+export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data)
.then(res => res.json())
.then(res => {
- commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
+ if (res.discussion) {
+ commit(types.UPDATE_DISCUSSION, res.discussion);
+
+ updateOrCreateNotes({ commit, state, getters, dispatch }, res.discussion.notes);
+
+ dispatch('updateMergeRequestWidget');
+ dispatch('startTaskList');
+ dispatch('updateResolvableDiscussonsCounts');
+ } else {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
+ }
return res;
});
@@ -262,25 +294,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (resp.notes && resp.notes.length) {
- const { notesById } = getters;
-
- resp.notes.forEach(note => {
- if (notesById[note.id]) {
- commit(types.UPDATE_NOTE, note);
- } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
- const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
-
- if (discussion) {
- commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
- } else if (note.type === constants.DIFF_NOTE) {
- dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
- } else {
- commit(types.ADD_NEW_NOTE, note);
- }
- } else {
- commit(types.ADD_NEW_NOTE, note);
- }
- });
+ updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes);
dispatch('startTaskList');
}
@@ -415,15 +429,22 @@ export const submitSuggestion = (
commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId });
callback();
})
- .catch(() => {
- Flash(
- __('Something went wrong while applying the suggestion. Please try again.'),
- 'alert',
- flashContainer,
+ .catch(err => {
+ const defaultMessage = __(
+ 'Something went wrong while applying the suggestion. Please try again.',
);
+ const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage;
+
+ Flash(__(flashMessage), 'alert', flashContainer);
callback();
});
};
+export const convertToDiscussion = ({ commit }, noteId) =>
+ commit(types.CONVERT_TO_DISCUSSION, noteId);
+
+export const removeConvertedDiscussion = ({ commit }, noteId) =>
+ commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 0ffc0cb2593..fcc8889b0c7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -4,6 +4,8 @@ import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
+export const convertedDisscussionIds = state => state.convertedDisscussionIds;
+
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;
@@ -189,6 +191,9 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
return getters.unresolvedDiscussionsIdsByDate[0];
};
+export const getDiscussion = state => discussionId =>
+ state.discussions.find(discussion => discussion.id === discussionId);
+
export const commentsDisabled = state => state.commentsDisabled;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 887e6d22b06..6168aeae35d 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -5,6 +5,7 @@ import mutations from '../mutations';
export default () => ({
state: {
discussions: [],
+ convertedDisscussionIds: [],
targetNoteHash: null,
lastFetchedAt: null,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index df943c155f4..796370920bb 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -17,6 +17,8 @@ export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const APPLY_SUGGESTION = 'APPLY_SUGGESTION';
+export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
+export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 8992454be2e..fa44ef2d057 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -105,7 +105,10 @@ export default {
if (discussion.diff_file) {
diffData.file_hash = discussion.diff_file.file_hash;
- diffData.truncated_diff_lines = discussion.truncated_diff_lines || [];
+
+ diffData.truncated_diff_lines = utils.prepareDiffLines(
+ discussion.truncated_diff_lines || [],
+ );
}
// To support legacy notes, should be very rare case.
@@ -190,6 +193,10 @@ export default {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
+ if (note.type === constants.DISCUSSION_NOTE) {
+ noteObj.individual_note = false;
+ }
+
noteObj.notes.splice(0, 1, note);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
@@ -243,7 +250,7 @@ export default {
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- discussion.truncated_diff_lines = diffLines;
+ discussion.truncated_diff_lines = utils.prepareDiffLines(diffLines);
},
[types.DISABLE_COMMENTS](state, value) {
@@ -261,4 +268,16 @@ export default {
).length;
state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1;
},
+
+ [types.CONVERT_TO_DISCUSSION](state, discussionId) {
+ const convertedDisscussionIds = [...state.convertedDisscussionIds, discussionId];
+ Object.assign(state, { convertedDisscussionIds });
+ },
+
+ [types.REMOVE_CONVERTED_DISCUSSION](state, discussionId) {
+ const convertedDisscussionIds = [...state.convertedDisscussionIds];
+
+ convertedDisscussionIds.splice(convertedDisscussionIds.indexOf(discussionId), 1);
+ Object.assign(state, { convertedDisscussionIds });
+ },
};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index dd57539e4d8..4b0feb0f94d 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -1,4 +1,5 @@
import AjaxCache from '~/lib/utils/ajax_cache';
+import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
@@ -28,3 +29,6 @@ export const getQuickActionText = note => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
+
+export const prepareDiffLines = diffLines =>
+ diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) }));
diff --git a/app/assets/javascripts/pages/admin/application_settings/show/index.js b/app/assets/javascripts/pages/admin/application_settings/show/index.js
new file mode 100644
index 00000000000..5ec9688a6e4
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/show/index.js
@@ -0,0 +1,3 @@
+import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits';
+
+document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder());
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 3aa793e47b9..8a32556f06c 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,7 +1,3 @@
import initAdmin from './admin';
-import initUserInternalRegexPlaceholder from './application_settings/account_and_limits';
-document.addEventListener('DOMContentLoaded', () => {
- initAdmin();
- initUserInternalRegexPlaceholder();
-});
+document.addEventListener('DOMContentLoaded', initAdmin());
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index 260484726f3..ff758fcb4fe 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,10 +1,11 @@
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
- IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+ addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js
index 8f98be79640..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index.js
@@ -1,7 +1,5 @@
import ProjectsList from '~/projects_list';
-import Star from '../../../star';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
- new Star('.project-row'); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js
index 0c585e162cb..01001d4f3ff 100644
--- a/app/assets/javascripts/pages/explore/projects/index.js
+++ b/app/assets/javascripts/pages/explore/projects/index.js
@@ -1,3 +1,5 @@
import ProjectsList from '~/projects_list';
-document.addEventListener('DOMContentLoaded', () => new ProjectsList());
+document.addEventListener('DOMContentLoaded', () => {
+ new ProjectsList(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/edit/index.js
index 8001d2dd1da..8001d2dd1da 100644
--- a/app/assets/javascripts/pages/groups/clusters/update/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/edit/index.js
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
index 845a5f7042c..30d519d0e37 100644
--- a/app/assets/javascripts/pages/groups/clusters/index/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -1,5 +1,6 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
});
diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js
index c22a164cd4e..e4f4c3b574e 100644
--- a/app/assets/javascripts/pages/groups/group_members/index/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index/index.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
import memberExpirationDate from '~/member_expiration_date';
-import Members from '~/members';
+import Members from 'ee_else_ce/members';
import UsersSelect from '~/users_select';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
index bf80d8b8193..451be6497de 100644
--- a/app/assets/javascripts/pages/groups/index.js
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -1,6 +1,11 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+function initGcpSignupCallout() {
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
+}
+
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
@@ -10,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- initDismissableCallout('.gcp-signup-offer');
+ initGcpSignupCallout();
initGkeDropdowns();
}
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 736c6a62610..35d4b034654 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,9 +1,11 @@
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
document.addEventListener('DOMContentLoaded', () => {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 339ce67438a..12a26fd88fa 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,10 +1,11 @@
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
- IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+ addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js
new file mode 100644
index 00000000000..dcd84f0faf9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/gitea/status/index.js
@@ -0,0 +1,7 @@
+import mountImportProjectsTable from '~/import_projects';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('import-projects-mount-element');
+
+ mountImportProjectsTable(mountElement);
+});
diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js
new file mode 100644
index 00000000000..dcd84f0faf9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/github/status/index.js
@@ -0,0 +1,7 @@
+import mountImportProjectsTable from '~/import_projects';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('import-projects-mount-element');
+
+ mountImportProjectsTable(mountElement);
+});
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index c7ce4675573..c9d3bbc8c39 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
+import emojiRegex from 'emoji-regex';
import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
@@ -42,32 +43,47 @@ document.addEventListener('DOMContentLoaded', () => {
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
+ const userNameInput = document.getElementById('user_name');
+ userNameInput.addEventListener('input', () => {
+ const EMOJI_REGEX = emojiRegex();
+ if (EMOJI_REGEX.test(userNameInput.value)) {
+ // set field to invalid so it gets detected by GlFieldErrors
+ userNameInput.setCustomValidity('Invalid field');
+ } else {
+ userNameInput.setCustomValidity('');
+ }
+ });
+
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
- const emojiMenu = new EmojiMenu(
- Emoji,
- toggleEmojiMenuButtonSelector,
- 'js-status-emoji-menu',
- selectEmojiCallback,
- );
- emojiMenu.bindEvents();
+ Emoji.initEmojiMap()
+ .then(() => {
+ const emojiMenu = new EmojiMenu(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ 'js-status-emoji-menu',
+ selectEmojiCallback,
+ );
+ emojiMenu.bindEvents();
- const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
- statusMessageField.addEventListener('input', () => {
- const hasStatusMessage = statusMessageField.value.trim() !== '';
- const statusEmoji = findStatusEmoji();
- if (hasStatusMessage && statusEmoji) {
- return;
- }
+ const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
+ statusMessageField.addEventListener('input', () => {
+ const hasStatusMessage = statusMessageField.value.trim() !== '';
+ const statusEmoji = findStatusEmoji();
+ if (hasStatusMessage && statusEmoji) {
+ return;
+ }
- if (hasStatusMessage) {
- toggleNoEmojiPlaceholder(false);
- toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
- } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
- toggleNoEmojiPlaceholder(true);
- removeStatusEmoji();
- }
- });
+ if (hasStatusMessage) {
+ toggleNoEmojiPlaceholder(false);
+ toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
+ } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
+ toggleNoEmojiPlaceholder(true);
+ removeStatusEmoji();
+ }
+ });
+ })
+ .catch(() => createFlash('Failed to load emoji list.'));
})
.catch(() => createFlash('Failed to load emoji list.'));
});
diff --git a/app/assets/javascripts/pages/projects/clusters/update/index.js b/app/assets/javascripts/pages/projects/clusters/edit/index.js
index 8001d2dd1da..8001d2dd1da 100644
--- a/app/assets/javascripts/pages/projects/clusters/update/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/edit/index.js
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index 845a5f7042c..30d519d0e37 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,6 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
});
diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js
index 0b644780ad4..0d69a689316 100644
--- a/app/assets/javascripts/pages/projects/environments/metrics/index.js
+++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js
@@ -1,3 +1,3 @@
-import monitoringBundle from '~/monitoring/monitoring_bundle';
+import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle';
document.addEventListener('DOMContentLoaded', monitoringBundle);
diff --git a/app/assets/javascripts/pages/projects/error_tracking/index.js b/app/assets/javascripts/pages/projects/error_tracking/index.js
new file mode 100644
index 00000000000..5a8fe137e9a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/error_tracking/index.js
@@ -0,0 +1,5 @@
+import ErrorTracking from '~/error_tracking';
+
+document.addEventListener('DOMContentLoaded', () => {
+ ErrorTracking();
+});
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 3ccad513c05..314519ee442 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -2,65 +2,86 @@ import $ from 'jquery';
import Chart from 'chart.js';
import _ from 'underscore';
+import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils';
+
document.addEventListener('DOMContentLoaded', () => {
const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML);
- const responsiveChart = (selector, data) => {
- const options = {
- scaleOverlay: true,
- responsive: true,
- pointHitDetectionRadius: 2,
- maintainAspectRatio: false,
- };
+ const barChart = (selector, data) => {
// get selector by context
const ctx = selector.get(0).getContext('2d');
// pointing parent container to make chart.js inherit its width
const container = $(selector).parent();
- const generateChart = () => {
- 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();
+ selector.attr('width', $(container).width());
+
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ const shouldAdjustFontSize = window.innerWidth < 768;
+ return new Chart(ctx, {
+ type: 'bar',
+ data,
+ options: barChartOptions(shouldAdjustFontSize),
+ });
+ };
+
+ const pieChart = (context, data) => {
+ const options = pieChartOptions();
+
+ return new Chart(context, {
+ type: 'pie',
+ data,
+ options,
+ });
};
const chartData = data => ({
labels: Object.keys(data),
datasets: [
{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
+ backgroundColor: 'rgba(220,220,220,0.5)',
+ borderColor: 'rgba(220,220,220,1)',
+ borderWidth: 1,
data: _.values(data),
},
],
});
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
+
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
+
const hourData = chartData(projectChartData.hour);
- responsiveChart($('#hour-chart'), hourData);
+ barChart($('#hour-chart'), hourData);
- const dayData = chartData(projectChartData.weekDays);
- responsiveChart($('#weekday-chart'), dayData);
+ const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week);
+ const dayData = chartData(weekDays);
+ barChart($('#weekday-chart'), dayData);
const monthData = chartData(projectChartData.month);
- responsiveChart($('#month-chart'), monthData);
+ barChart($('#month-chart'), monthData);
- const data = projectChartData.languages;
+ const data = {
+ datasets: [
+ {
+ data: projectChartData.languages.map(x => x.value),
+ backgroundColor: projectChartData.languages.map(x => x.color),
+ hoverBackgroundColor: projectChartData.languages.map(x => x.highlight),
+ },
+ ],
+ labels: projectChartData.languages.map(x => x.label),
+ };
const ctx = $('#languages-chart')
.get(0)
.getContext('2d');
- const options = {
- scaleOverlay: true,
- responsive: true,
- maintainAspectRatio: false,
- };
-
- new Chart(ctx).Pie(data, options);
+ pieChart(ctx, data);
});
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 5659e13981a..d4bd02c14e9 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,5 @@
-import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
@@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
+
initGkeDropdowns();
}
diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js
index ffc84dc106b..aecc6484b26 100644
--- a/app/assets/javascripts/pages/projects/issues/edit/index.js
+++ b/app/assets/javascripts/pages/projects/issues/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from '../form';
+import initForm from 'ee_else_ce/pages/projects/issues/form';
document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index f99023ad8e7..941c4552579 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import GLForm from '~/gl_form';
-import IssuableForm from '~/issuable_form';
+import IssuableForm from 'ee_else_ce/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index a56c0bb6be8..8bf0c2edc71 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -4,11 +4,13 @@ import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js
index ffc84dc106b..aecc6484b26 100644
--- a/app/assets/javascripts/pages/projects/issues/new/index.js
+++ b/app/assets/javascripts/pages/projects/issues/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '../form';
+import initForm from 'ee_else_ce/pages/projects/issues/form';
document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index ec39db12e74..0bcca22e40f 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -2,12 +2,13 @@ import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
- IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
+ addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index e3971618da5..8f0dc8554e2 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import Diff from '~/diff';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
-import IssuableForm from '~/issuable_form';
+import IssuableForm from 'ee_else_ce/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index db2a4041ec0..bd4309e47ad 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -70,7 +70,7 @@ export default {
:checked="isEditable"
class="label-bold"
type="radio"
- @click="toggleCustomInput(true);"
+ @click="toggleCustomInput(true)"
/>
<label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label>
@@ -88,7 +88,7 @@ export default {
:value="cronIntervalPresets.everyDay"
class="label-bold"
type="radio"
- @click="toggleCustomInput(false);"
+ @click="toggleCustomInput(false)"
/>
<label class="label-bold" for="every-day"> {{ __('Every day (at 4:00am)') }} </label>
@@ -102,7 +102,7 @@ export default {
:value="cronIntervalPresets.everyWeek"
class="label-bold"
type="radio"
- @click="toggleCustomInput(false);"
+ @click="toggleCustomInput(false)"
/>
<label class="label-bold" for="every-week">
@@ -118,7 +118,7 @@ export default {
:value="cronIntervalPresets.everyMonth"
class="label-bold"
type="radio"
- @click="toggleCustomInput(false);"
+ @click="toggleCustomInput(false)"
/>
<label class="label-bold" for="every-month">
diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
index 48353f3b4ef..9fa580d2ba9 100644
--- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
@@ -1,29 +1,31 @@
import $ from 'jquery';
import Chart from 'chart.js';
-const options = {
- scaleOverlay: true,
- responsive: true,
- maintainAspectRatio: false,
-};
+import { barChartOptions, lineChartOptions } from '~/lib/utils/chart_utils';
+
+const SUCCESS_LINE_COLOR = '#1aaa55';
+
+const TOTAL_LINE_COLOR = '#707070';
-const buildChart = chartScope => {
+const buildChart = (chartScope, shouldAdjustFontSize) => {
const data = {
labels: chartScope.labels,
datasets: [
{
- fillColor: '#707070',
- strokeColor: '#707070',
- pointColor: '#707070',
- pointStrokeColor: '#EEE',
- data: chartScope.totalValues,
+ backgroundColor: SUCCESS_LINE_COLOR,
+ borderColor: SUCCESS_LINE_COLOR,
+ pointBackgroundColor: SUCCESS_LINE_COLOR,
+ pointBorderColor: '#fff',
+ data: chartScope.successValues,
+ fill: 'origin',
},
{
- fillColor: '#1aaa55',
- strokeColor: '#1aaa55',
- pointColor: '#1aaa55',
- pointStrokeColor: '#fff',
- data: chartScope.successValues,
+ backgroundColor: TOTAL_LINE_COLOR,
+ borderColor: TOTAL_LINE_COLOR,
+ pointBackgroundColor: TOTAL_LINE_COLOR,
+ pointBorderColor: '#EEE',
+ data: chartScope.totalValues,
+ fill: '-1',
},
],
};
@@ -31,36 +33,51 @@ const buildChart = chartScope => {
.get(0)
.getContext('2d');
- new Chart(ctx).Line(data, options);
+ return new Chart(ctx, {
+ type: 'line',
+ data,
+ options: lineChartOptions({
+ width: ctx.canvas.width,
+ numberOfPoints: chartScope.totalValues.length,
+ shouldAdjustFontSize,
+ }),
+ });
};
-document.addEventListener('DOMContentLoaded', () => {
- const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
- const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
+const buildBarChart = (chartTimesData, shouldAdjustFontSize) => {
const data = {
labels: chartTimesData.labels,
datasets: [
{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
+ backgroundColor: 'rgba(220,220,220,0.5)',
+ borderColor: 'rgba(220,220,220,1)',
+ borderWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartTimesData.values,
},
],
};
-
- if (window.innerWidth < 768) {
- // Scale fonts if window width lower than 768px (iPad portrait)
- options.scaleFontSize = 8;
- }
-
- new Chart(
+ return new Chart(
$('#build_timesChart')
.get(0)
.getContext('2d'),
- ).Bar(data, options);
+ {
+ type: 'bar',
+ data,
+ options: barChartOptions(shouldAdjustFontSize),
+ },
+ );
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
+ const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
+
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ const shouldAdjustFontSize = window.innerWidth < 768;
+
+ buildBarChart(chartTimesData, shouldAdjustFontSize);
- chartsData.forEach(scope => buildChart(scope));
+ chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
});
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index adbe744290a..f39765818e7 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -1,7 +1,7 @@
+import Members from 'ee_else_ce/members';
import memberExpirationDate from '../../../member_expiration_date';
import UsersSelect from '../../../users_select';
import groupsSelect from '../../../groups_select';
-import Members from '../../../members';
document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups');
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
new file mode 100644
index 00000000000..73c745179be
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -0,0 +1,5 @@
+import mountErrorTrackingForm from '~/error_tracking_settings';
+
+document.addEventListener('DOMContentLoaded', () => {
+ mountErrorTrackingForm();
+});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 08c7719dcf2..19d9903c988 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -325,8 +325,8 @@ export default {
<project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
:help-path="pagesHelpPath"
- label="Pages"
- help-text="Static website for the project."
+ label="Pages access control"
+ help-text="Access control for the project's static website"
>
<project-feature-setting
v-model="pagesAccessLevel"
diff --git a/app/assets/javascripts/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js
new file mode 100644
index 00000000000..d6afc71fb03
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/tags/releases/index.js
@@ -0,0 +1,8 @@
+import $ from 'jquery';
+import ZenMode from '~/zen_mode';
+import GLForm from '~/gl_form';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ZenMode(); // eslint-disable-line no-new
+ new GLForm($('.release-form')); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 07f32210d93..e1a3f42a71f 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,13 +1,20 @@
import $ from 'jquery';
import UsernameValidator from './username_validator';
+import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
+import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
+ new NoEmojiValidator(); // eslint-disable-line no-new
new OAuthRememberMe({
container: $('.omniauth-container'),
}).bindEvents();
+
+ // Save the URL fragment from the current window location. This will be present if the user was
+ // redirected to sign-in after attempting to access a protected URL that included a fragment.
+ preserveUrlFragment(window.location.hash);
});
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index 761618109a4..191221a48cd 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
/**
* OAuth-based login buttons have a separate "remember me" checkbox.
@@ -24,9 +25,9 @@ export default class OAuthRememberMe {
const href = $(element).attr('href');
if (rememberMe) {
- $(element).attr('href', `${href}?remember_me=1`);
+ $(element).attr('href', mergeUrlParams({ remember_me: 1 }, href));
} else {
- $(element).attr('href', href.replace('?remember_me=1', ''));
+ $(element).attr('href', removeParams(['remember_me'], href));
}
});
}
diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js
new file mode 100644
index 00000000000..e617fecaa0f
--- /dev/null
+++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js
@@ -0,0 +1,32 @@
+import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility';
+
+/**
+ * Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and
+ * OAuth/SAML login links.
+ *
+ * @param fragment {string} - url fragment to be preserved
+ */
+export default function preserveUrlFragment(fragment = '') {
+ if (fragment) {
+ const normalFragment = fragment.replace(/^#/, '');
+
+ // Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is
+ // eventually redirected back to the originally requested URL.
+ const forms = document.querySelectorAll('#signin-container form');
+ Array.prototype.forEach.call(forms, form => {
+ const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
+ form.setAttribute('action', actionWithFragment);
+ });
+
+ // Append a redirect_fragment query param to all oauth provider links. The redirect_fragment
+ // query param will be available in the omniauth callback upon successful authentication
+ const anchors = document.querySelectorAll('#signin-container a.oauth-login');
+ Array.prototype.forEach.call(anchors, anchor => {
+ const newHref = mergeUrlParams(
+ { redirect_fragment: normalFragment },
+ anchor.getAttribute('href'),
+ );
+ anchor.setAttribute('href', newHref);
+ });
+ }
+}
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8a84ac37dab..61204c37307 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -10,6 +10,12 @@ import { __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold };
+const firstDayOfWeekChoices = Object.freeze({
+ sunday: 0,
+ monday: 1,
+ saturday: 6,
+});
+
const LOADING_HTML = `
<div class="text-center">
<i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
@@ -49,7 +55,7 @@ export default class ActivityCalendar {
timestamps,
calendarActivitiesPath,
utcOffset = 0,
- firstDayOfWeek = 0,
+ firstDayOfWeek = firstDayOfWeekChoices.sunday,
monthsAgo = 12,
) {
this.calendarActivitiesPath = calendarActivitiesPath;
@@ -159,7 +165,7 @@ export default class ActivityCalendar {
.append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
- if (a === 0 && stamp.day === 0) {
+ if (a === 0 && stamp.day === this.firstDayOfWeek) {
const month = stamp.date.getMonth();
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
@@ -205,6 +211,19 @@ export default class ActivityCalendar {
y: 29 + this.dayYPos(5),
},
];
+
+ if (this.firstDayOfWeek === firstDayOfWeekChoices.monday) {
+ days.push({
+ text: 'S',
+ y: 29 + this.dayYPos(7),
+ });
+ } else if (this.firstDayOfWeek === firstDayOfWeekChoices.saturday) {
+ days.push({
+ text: 'S',
+ y: 29 + this.dayYPos(6),
+ });
+ }
+
this.svg
.append('g')
.selectAll('text')
diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js
index eec2b5ca8e5..a7c3c9d104d 100644
--- a/app/assets/javascripts/pages/users/user_overview_block.js
+++ b/app/assets/javascripts/pages/users/user_overview_block.js
@@ -15,7 +15,8 @@ export default class UserOverviewBlock {
}
loadData() {
- const loadingEl = document.querySelector(`${this.container} .loading`);
+ const containerEl = document.querySelector(this.container);
+ const loadingEl = containerEl.querySelector(`.loading`);
loadingEl.classList.remove('hide');
@@ -29,18 +30,21 @@ export default class UserOverviewBlock {
render(data) {
const { html, count } = data;
- const contentList = document.querySelector(`${this.container} .overview-content-list`);
+ const containerEl = document.querySelector(this.container);
+ const contentList = containerEl.querySelector('.overview-content-list');
contentList.innerHTML += html;
- const loadingEl = document.querySelector(`${this.container} .loading`);
+ const loadingEl = containerEl.querySelector('.loading');
if (count && count > 0) {
- document.querySelector(`${this.container} .js-view-all`).classList.remove('hide');
+ containerEl.querySelector('.js-view-all').classList.remove('hide');
} else {
- document
- .querySelector(`${this.container} .nothing-here-block`)
- .classList.add('text-left', 'p-0');
+ const nothingHereBlock = containerEl.querySelector('.nothing-here-block');
+
+ if (nothingHereBlock) {
+ nothingHereBlock.classList.add('p-5');
+ }
}
loadingEl.classList.add('hide');
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 1c3fd58ca74..636308c5401 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -221,7 +221,7 @@ export default class UserTabs {
const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap);
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset');
- const calendarHint = __('Issues, merge requests, pushes and comments.');
+ const calendarHint = __('Issues, merge requests, pushes, and comments.');
$calendarWrap.html(CALENDAR_TEMPLATE);
@@ -234,7 +234,7 @@ export default class UserTabs {
data,
calendarActivitiesPath,
utcOffset,
- 0,
+ gon.first_day_of_week,
monthsAgo,
);
}
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 74faa35358d..1ec2784cc5a 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -134,6 +134,13 @@ export default {
>ms / <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span> gc
</span>
</div>
+ <div
+ v-if="currentRequest.details && currentRequest.details.tracing"
+ id="peek-view-trace"
+ class="view"
+ >
+ <a :href="currentRequest.details.tracing.tracing_url"> trace </a>
+ </div>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
new file mode 100644
index 00000000000..4a08e158f6b
--- /dev/null
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -0,0 +1,42 @@
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
+import Flash from './flash';
+
+export default class PersistentUserCallout {
+ constructor(container) {
+ const { dismissEndpoint, featureId } = container.dataset;
+ this.container = container;
+ this.dismissEndpoint = dismissEndpoint;
+ this.featureId = featureId;
+
+ this.init();
+ }
+
+ init() {
+ const closeButton = this.container.querySelector('.js-close');
+ closeButton.addEventListener('click', event => this.dismiss(event));
+ }
+
+ dismiss(event) {
+ event.preventDefault();
+
+ axios
+ .post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ })
+ .then(() => {
+ this.container.remove();
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ }
+
+ static factory(container) {
+ if (!container) {
+ return undefined;
+ }
+
+ return new PersistentUserCallout(container);
+ }
+}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 59cebaba717..a49dc311bd0 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,59 +1,20 @@
<script>
-import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from './stage_column_component.vue';
+import GraphMixin from '../../mixins/graph_component_mixin';
export default {
components: {
StageColumnComponent,
GlLoadingIcon,
},
- props: {
- isLoading: {
- type: Boolean,
- required: true,
- },
- pipeline: {
- type: Object,
- required: true,
- },
- },
- computed: {
- graph() {
- return this.pipeline.details && this.pipeline.details.stages;
- },
- },
- methods: {
- capitalizeStageName(name) {
- const escapedName = _.escape(name);
- return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
- },
- isFirstColumn(index) {
- return index === 0;
- },
- stageConnectorClass(index, stage) {
- let className;
-
- // If it's the first stage column and only has one job
- if (index === 0 && stage.groups.length === 1) {
- className = 'no-margin';
- } else if (index > 0) {
- // If it is not the first column
- className = 'left-margin';
- }
-
- return className;
- },
- refreshPipelineGraph() {
- this.$emit('refreshPipelineGraph');
- },
- },
+ mixins: [GraphMixin],
};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph pipeline-tab-content">
- <div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div>
+ <div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div>
<ul v-if="!isLoading" class="stage-column-list">
<stage-column-component
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index cf9db89e32b..2b32a6e4a98 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -108,7 +108,7 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
- class="js-pipeline-graph-job-link"
+ class="js-pipeline-graph-job-link qa-job-link"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 30a5bbf92ce..918622ef8dc 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,8 +1,20 @@
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import _ from 'underscore';
+import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
+const popoverTitle = sprintf(
+ _.escape(
+ __(
+ `This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}`,
+ ),
+ ),
+ { strongStart: '<b>', strongEnd: '</b>' },
+ false,
+);
+
export default {
components: {
UserAvatarLink,
@@ -32,14 +44,14 @@ export default {
trigger: 'focus',
placement: 'top',
title: `<div class="autodevops-title">
- This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>
+ ${popoverTitle}
</div>`,
content: `<a
class="autodevops-link"
href="${this.autoDevopsHelpPath}"
target="_blank"
rel="noopener noreferrer nofollow">
- Learn more about Auto DevOps
+ ${_.escape(__('Learn more about Auto DevOps'))}
</a>`,
};
},
@@ -54,9 +66,9 @@ export default {
<span>by</span>
<user-avatar-link
v-if="user"
- :link-href="pipeline.user.path"
- :img-src="pipeline.user.avatar_url"
- :tooltip-text="pipeline.user.name"
+ :link-href="user.path"
+ :img-src="user.avatar_url"
+ :tooltip-text="user.name"
class="js-pipeline-url-user"
/>
<span v-if="!user" class="js-pipeline-url-api api"> API </span>
@@ -64,10 +76,10 @@ export default {
<span
v-if="pipeline.flags.latest"
v-gl-tooltip
+ :title="__('Latest pipeline for this branch')"
class="js-pipeline-url-latest badge badge-success"
- title="__('Latest pipeline for this branch')"
>
- latest
+ {{ __('latest') }}
</span>
<span
v-if="pipeline.flags.yaml_errors"
@@ -75,7 +87,7 @@ export default {
:title="pipeline.yaml_errors"
class="js-pipeline-url-yaml badge badge-danger"
>
- yaml invalid
+ {{ __('yaml invalid') }}
</span>
<span
v-if="pipeline.flags.failure_reason"
@@ -83,7 +95,7 @@ export default {
:title="pipeline.failure_reason"
class="js-pipeline-url-failure badge badge-danger"
>
- error
+ {{ __('error') }}
</span>
<gl-link
v-if="pipeline.flags.auto_devops"
@@ -95,15 +107,15 @@ export default {
Auto DevOps
</gl-link>
<span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck badge badge-warning">
- stuck
+ {{ __('stuck') }}
</span>
<span
v-if="pipeline.flags.merge_request"
v-gl-tooltip
- title="__('This pipeline is run in a merge request context')"
+ :title="__('This pipeline is run in a merge request context')"
class="js-pipeline-url-mergerequest badge badge-info"
>
- merge request
+ {{ __('merge request') }}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 2e9f2519fcb..244d332f38f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -59,28 +59,30 @@ export default {
</script>
<template>
<div class="btn-group">
- <gl-button
+ <button
v-gl-tooltip
+ type="button"
:disabled="isLoading"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
- title="Manual job"
+ :title="__('Manual job')"
data-toggle="dropdown"
- aria-label="Manual job"
+ :aria-label="__('Manual job')"
>
- <icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
+ <icon name="play" class="icon-play" />
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
<gl-loading-icon v-if="isLoading" />
- </gl-button>
+ </button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="action in actions" :key="action.path">
<gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- class="js-pipeline-action-link no-btn btn"
- @click="onClickAction(action);"
+ class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap"
+ @click="onClickAction(action)"
>
{{ action.name }}
- <span v-if="action.scheduled_at" class="pull-right">
+ <span v-if="action.scheduled_at">
<icon name="clock" />
<gl-countdown :end-date-string="action.scheduled_at" />
</span>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 908b10afee6..2ab0ad4d013 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -9,7 +9,6 @@ export default {
components: {
Icon,
GlLink,
- GlButton,
},
props: {
artifacts: {
@@ -21,20 +20,22 @@ export default {
</script>
<template>
<div class="btn-group" role="group">
- <gl-button
+ <button
v-gl-tooltip
- class="dropdown-toggle build-artifacts js-pipeline-dropdown-download"
- title="Artifacts"
+ type="button"
+ class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download"
+ :title="__('Artifacts')"
data-toggle="dropdown"
- aria-label="Artifacts"
+ :aria-label="__('Artifacts')"
>
- <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
- </gl-button>
+ <icon name="download" />
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="(artifact, i) in artifacts" :key="i">
- <gl-link :href="artifact.path" rel="nofollow" download>
- Download {{ artifact.name }} artifacts
- </gl-link>
+ <gl-link :href="artifact.path" rel="nofollow" download
+ >Download {{ artifact.name }} artifacts</gl-link
+ >
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 2d3f667e73e..7426936515a 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -172,8 +172,6 @@ export default {
<span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events">
<icon :name="borderlessIcon" />
</span>
-
- <i class="fa fa-caret-down" aria-hidden="true"> </i>
</button>
<div
diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
new file mode 100644
index 00000000000..66e9476dadf
--- /dev/null
+++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
@@ -0,0 +1,44 @@
+import _ from 'underscore';
+
+export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ graph() {
+ return this.pipeline.details && this.pipeline.details.stages;
+ },
+ },
+ methods: {
+ capitalizeStageName(name) {
+ const escapedName = _.escape(name);
+ return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
+ },
+ isFirstColumn(index) {
+ return index === 0;
+ },
+ stageConnectorClass(index, stage) {
+ let className;
+
+ // If it's the first stage column and only has one job
+ if (index === 0 && stage.groups.length === 1) {
+ className = 'no-margin';
+ } else if (index > 0) {
+ // If it is not the first column
+ className = 'left-margin';
+ }
+
+ return className;
+ },
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
+ },
+};
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 32bfa47e5f2..3cc9d0a3a4e 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -27,11 +27,7 @@ export default {
},
computed: {
shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
+ return !this.isLoading;
},
},
beforeMount() {
@@ -94,8 +90,7 @@ export default {
this.isLoading = false;
this.successCallback(response);
- // restart polling
- this.poll.restart({ data: this.requestData });
+ this.poll.enable({ data: this.requestData, response });
})
.catch(() => {
this.isLoading = false;
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index a33835472bb..5ee510eb11d 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,97 +5,101 @@ import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
export default function projectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- this.groupId = $(select).data('groupId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ this.groupId = $(select).data('groupId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = 'Search for project';
- if (this.includeGroups) {
- placeholder += ' or group';
- }
+ placeholder = 'Search for project';
+ if (this.includeGroups) {
+ placeholder += ' or group';
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
- var data;
- data = {
- results: projects,
- };
- return query.callback(data);
- };
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function(_this) {
+ return function(query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
var data;
- data = groups.concat(projects);
- return finalCallback(data);
+ data = {
+ results: projects,
+ };
+ return query.callback(data);
};
- return Api.groups(query.term, {}, groupsCallback);
+ if (_this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(
+ _this.groupId,
+ query.term,
+ {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ with_shared: _this.withShared,
+ include_subgroups: _this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ } else {
+ return Api.projects(
+ query.term,
+ {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ },
+ projectsCallback,
+ );
+ }
};
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(
- _this.groupId,
- query.term,
- {
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- with_shared: _this.withShared,
- include_subgroups: _this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
- } else {
- return Api.projects(
- query.term,
- {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- },
- projectsCallback,
- );
- }
- };
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function(project) {
- return project.name_with_namespace || project.name;
- },
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text: function(project) {
+ return project.name_with_namespace || project.name;
+ },
- initSelection: function(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection: function(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 3dbac3ff942..d3b5f532dc1 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -44,9 +44,13 @@ export default class ProjectSelectComboButton {
// eslint-disable-next-line class-methods-use-this
openDropdown(event) {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
}
selectProject() {
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
index 21095fcba16..83811ab489a 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -108,9 +108,7 @@ export default {
</span>
</li>
<li v-for="result in results" :key="result.id">
- <button type="button" @click.prevent="setItem(result.name);">
- {{ result.name }}
- </button>
+ <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
index 056584c8865..a2eb79af4f9 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -169,7 +169,7 @@ export default {
</span>
</li>
<li v-for="result in results" :key="result.project_number">
- <button type="button" @click.prevent="setItem(result);">{{ result.name }}</button>
+ <button type="button" @click.prevent="setItem(result)">{{ result.name }}</button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
index 728616a441f..fd5d5f86401 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -34,7 +34,7 @@ export default {
},
errorMessage() {
return sprintf(
- s__('ClusterIntegration|An error occured while trying to fetch project zones: %{error}'),
+ s__('ClusterIntegration|An error occurred while trying to fetch project zones: %{error}'),
{ error: this.gapiError },
);
},
@@ -82,9 +82,7 @@ export default {
</span>
</li>
<li v-for="result in results" :key="result.id">
- <button type="button" @click.prevent="setItem(result.name);">
- {{ result.name }}
- </button>
+ <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 998554d1be5..1ade1811033 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -115,15 +115,63 @@ const bindEvents = () => {
const templates = {
rails: {
text: 'Ruby on Rails',
- icon: '.template-option svg.icon-rails',
+ icon: '.template-option .icon-rails',
},
express: {
text: 'NodeJS Express',
- icon: '.template-option svg.icon-node-express',
+ icon: '.template-option .icon-express',
},
spring: {
text: 'Spring',
- icon: '.template-option svg.icon-java-spring',
+ icon: '.template-option .icon-spring',
+ },
+ dotnetcore: {
+ text: '.NET Core',
+ icon: '.template-option .icon-dotnet',
+ },
+ gomicro: {
+ text: 'Go Micro',
+ icon: '.template-option .icon-gomicro',
+ },
+ hugo: {
+ text: 'Pages/Hugo',
+ icon: '.template-option .icon-hugo',
+ },
+ jekyll: {
+ text: 'Pages/Jekyll',
+ icon: '.template-option .icon-jekyll',
+ },
+ plainhtml: {
+ text: 'Pages/Plain HTML',
+ icon: '.template-option .icon-plainhtml',
+ },
+ gitbook: {
+ text: 'Pages/GitBook',
+ icon: '.template-option .icon-gitbook',
+ },
+ hexo: {
+ text: 'Pages/Hexo',
+ icon: '.template-option .icon-hexo',
+ },
+ nfhugo: {
+ text: 'Netlify/Hugo',
+ icon: '.template-option .icon-netlify',
+ },
+ nfjekyll: {
+ text: 'Netlify/Jekyll',
+ icon: '.template-option .icon-netlify',
+ },
+ nfplainhtml: {
+ text: 'Netlify/Plain HTML',
+ icon: '.template-option .icon-netlify',
+ },
+ nfgitbook: {
+ text: 'Netlify/GitBook',
+ icon: '.template-option .icon-netlify',
+ },
+ nfhexo: {
+ text: 'Netlify/Hexo',
+ icon: '.template-option .icon-netlify',
},
};
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 78c7671856a..81fe0a48c06 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -70,7 +70,7 @@ export default {
</thead>
<tbody>
<tr v-for="item in repo.list" :key="item.tag">
- <td>
+ <td class="monospace">
{{ item.tag }}
<clipboard-button
v-if="item.location"
@@ -80,7 +80,9 @@ export default {
/>
</td>
<td>
- <span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span>
+ <span v-gl-tooltip.bottom class="monospace" :title="item.revision">{{
+ item.shortRevision
+ }}</span>
</td>
<td>
{{ formatSize(item.size) }}
@@ -104,7 +106,7 @@ export default {
:aria-label="s__('ContainerRegistry|Remove tag')"
variant="danger"
class="js-delete-registry d-none d-sm-block float-right"
- @click="handleDeleteRegistry(item);"
+ @click="handleDeleteRegistry(item)"
>
<icon name="remove" />
</gl-button>
diff --git a/app/assets/javascripts/releases/components/app.vue b/app/assets/javascripts/releases/components/app.vue
index 0ad5ee2915c..5a06c4fec58 100644
--- a/app/assets/javascripts/releases/components/app.vue
+++ b/app/assets/javascripts/releases/components/app.vue
@@ -1,12 +1,12 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui';
import ReleaseBlock from './release_block.vue';
export default {
name: 'ReleasesApp',
components: {
- GlLoadingIcon,
+ GlSkeletonLoading,
GlEmptyState,
ReleaseBlock,
},
@@ -43,7 +43,7 @@ export default {
</script>
<template>
<div class="prepend-top-default">
- <gl-loading-icon v-if="isLoading" :size="2" class="js-loading prepend-top-20" />
+ <gl-skeleton-loading v-if="isLoading" class="js-loading" />
<gl-empty-state
v-else-if="shouldRenderEmptyState"
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 34b97826cdb..7ed1b407ddd 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -45,7 +45,7 @@ export default {
return this.release.author || {};
},
hasAuthor() {
- return _.isEmpty(this.author);
+ return !_.isEmpty(this.author);
},
},
};
@@ -87,7 +87,7 @@ export default {
<div
v-if="assets.links.length || assets.sources.length"
- Sclass="card-text prepend-top-default"
+ class="card-text prepend-top-default"
>
<b>
{{ __('Assets') }}
@@ -98,7 +98,7 @@ export default {
<li v-for="link in assets.links" :key="link.name" class="append-bottom-8">
<gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.url">
<icon name="package" class="align-middle append-right-4 align-text-bottom" />
- {{ link.name }}
+ {{ link.name }} <span v-if="link.external"> {{ __('(external source)') }}</span>
</gl-link>
</li>
</ul>
diff --git a/app/assets/javascripts/releases/store/actions.js b/app/assets/javascripts/releases/store/actions.js
index baa2251403e..e0a922d5ef6 100644
--- a/app/assets/javascripts/releases/store/actions.js
+++ b/app/assets/javascripts/releases/store/actions.js
@@ -11,7 +11,7 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
/**
* Fetches the main endpoint.
* Will dispatch requestNamespace action before starting the request.
- * Will dispatch receiveNamespaceSuccess if the request is successfull
+ * Will dispatch receiveNamespaceSuccess if the request is successful
* Will dispatch receiveNamesapceError if the request returns an error
*
* @param {String} projectId
@@ -30,7 +30,7 @@ export const receiveReleasesSuccess = ({ commit }, data) =>
export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
- createFlash(__('An error occured while fetching the releases. Please try again.'));
+ createFlash(__('An error occurred while fetching the releases. Please try again.'));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue
index 118e4b02c46..4f81cee2a38 100644
--- a/app/assets/javascripts/reports/components/modal_open_name.vue
+++ b/app/assets/javascripts/reports/components/modal_open_name.vue
@@ -26,7 +26,7 @@ export default {
<button
type="button"
class="btn-link btn-blank text-left break-link vulnerability-name-button"
- @click="handleIssueClick();"
+ @click="handleIssueClick()"
>
{{ issue.title }}
</button>
diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
index 938e83de546..7700f49bf7d 100644
--- a/app/assets/javascripts/reports/components/test_issue_body.vue
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -30,7 +30,7 @@ export default {
<button
type="button"
class="btn-link btn-blank text-left break-link vulnerability-name-button"
- @click="openModal({ issue });"
+ @click="openModal({ issue })"
>
<div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div>
{{ issue.name }}
diff --git a/app/assets/javascripts/serverless/components/environment_row.vue b/app/assets/javascripts/serverless/components/environment_row.vue
new file mode 100644
index 00000000000..4d18c5c4bdd
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/environment_row.vue
@@ -0,0 +1,65 @@
+<script>
+import FunctionRow from './function_row.vue';
+import ItemCaret from '~/groups/components/item_caret.vue';
+
+export default {
+ components: {
+ ItemCaret,
+ FunctionRow,
+ },
+ props: {
+ env: {
+ type: Array,
+ required: true,
+ },
+ envName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isOpen: true,
+ };
+ },
+ computed: {
+ envId() {
+ if (this.envName === '*') {
+ return 'env-global';
+ }
+
+ return `env-${this.envName}`;
+ },
+ isOpenClass() {
+ return {
+ 'is-open': this.isOpen,
+ };
+ },
+ },
+ methods: {
+ toggleOpen() {
+ this.isOpen = !this.isOpen;
+ },
+ },
+};
+</script>
+
+<template>
+ <li :id="envId" :class="isOpenClass" class="group-row has-children">
+ <div
+ class="group-row-contents d-flex justify-content-end align-items-center"
+ role="button"
+ @click.stop="toggleOpen"
+ >
+ <div class="folder-toggle-wrap d-flex align-items-center">
+ <item-caret :is-group-open="isOpen" />
+ </div>
+ <div class="group-text flex-grow title namespace-title prepend-left-default">
+ {{ envName }}
+ </div>
+ </div>
+ <ul v-if="isOpen" class="content-list group-list-tree">
+ <function-row v-for="(f, index) in env" :key="f.name" :index="index" :func="f" />
+ </ul>
+ </li>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
new file mode 100644
index 00000000000..4f89ad69129
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/function_details.vue
@@ -0,0 +1,56 @@
+<script>
+import PodBox from './pod_box.vue';
+import Url from './url.vue';
+
+export default {
+ components: {
+ PodBox,
+ Url,
+ },
+ props: {
+ func: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ name() {
+ return this.func.name;
+ },
+ description() {
+ return this.func.description;
+ },
+ funcUrl() {
+ return this.func.url;
+ },
+ podCount() {
+ return this.func.podcount || 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <section id="serverless-function-details">
+ <h3>{{ name }}</h3>
+ <div class="append-bottom-default">
+ <div v-for="(line, index) in description.split('\n')" :key="index">{{ line }}</div>
+ </div>
+ <url :uri="funcUrl" />
+
+ <h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4>
+ <div v-if="podCount > 0">
+ <p>
+ <b v-if="podCount == 1">{{ podCount }} {{ s__('ServerlessDetails|pod in use') }}</b>
+ <b v-else>{{ podCount }} {{ s__('ServerlessDetails|pods in use') }}</b>
+ </p>
+ <pod-box :count="podCount" />
+ <p>
+ {{
+ s__('ServerlessDetails|Number of Kubernetes pods in use over time based on necessity.')
+ }}
+ </p>
+ </div>
+ <div v-else><p>No pods loaded at this time.</p></div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
index 31f5427c771..773d18781fd 100644
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -1,9 +1,12 @@
<script>
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+import Url from './url.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
export default {
components: {
Timeago,
+ Url,
},
props: {
func: {
@@ -15,7 +18,18 @@ export default {
name() {
return this.func.name;
},
- url() {
+ description() {
+ const desc = this.func.description.split('\n');
+ if (desc.length > 1) {
+ return desc[1];
+ }
+
+ return desc[0];
+ },
+ detailUrl() {
+ return this.func.detail_url;
+ },
+ targetUrl() {
return this.func.url;
},
image() {
@@ -25,16 +39,34 @@ export default {
return this.func.created_at;
},
},
+ methods: {
+ checkClass(element) {
+ if (element.closest('.no-expand') === null) {
+ return true;
+ }
+
+ return false;
+ },
+ openDetails(e) {
+ if (this.checkClass(e.target)) {
+ visitUrl(this.detailUrl);
+ }
+ },
+ },
};
</script>
<template>
- <div class="gl-responsive-table-row">
- <div class="table-section section-20">{{ name }}</div>
- <div class="table-section section-50">
- <a :href="url">{{ url }}</a>
+ <li :id="name" class="group-row">
+ <div class="group-row-contents" role="button" @click="openDetails">
+ <p class="float-right text-right">
+ <span>{{ image }}</span
+ ><br />
+ <timeago :time="timestamp" />
+ </p>
+ <b>{{ name }}</b>
+ <div v-for="line in description.split('\n')" :key="line">{{ line }}</div>
+ <url :uri="targetUrl" class="prepend-top-8 no-expand" />
</div>
- <div class="table-section section-20">{{ image }}</div>
- <div class="table-section section-10"><timeago :time="timestamp" /></div>
- </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 349e14670b1..4bde409f906 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,19 +1,21 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
+import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
export default {
components: {
+ EnvironmentRow,
FunctionRow,
EmptyState,
GlSkeletonLoading,
},
props: {
functions: {
- type: Array,
+ type: Object,
required: true,
- default: () => [],
+ default: () => ({}),
},
installed: {
type: Boolean,
@@ -45,30 +47,21 @@ export default {
<section id="serverless-functions">
<div v-if="installed">
<div v-if="hasFunctionData">
- <div class="ci-table js-services-list function-element">
- <div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-20" role="rowheader">
- {{ s__('Serverless|Function') }}
- </div>
- <div class="table-section section-50" role="rowheader">
- {{ s__('Serverless|Domain') }}
- </div>
- <div class="table-section section-20" role="rowheader">
- {{ s__('Serverless|Runtime') }}
- </div>
- <div class="table-section section-10" role="rowheader">
- {{ s__('Serverless|Last Update') }}
- </div>
+ <template v-if="loadingData">
+ <div v-for="j in 3" :key="j" class="gl-responsive-table-row"><gl-skeleton-loading /></div>
+ </template>
+ <template v-else>
+ <div class="groups-list-tree-container">
+ <ul class="content-list group-list-tree">
+ <environment-row
+ v-for="(env, index) in functions"
+ :key="index"
+ :env="env"
+ :env-name="index"
+ />
+ </ul>
</div>
- <template v-if="loadingData">
- <div v-for="j in 3" :key="j" class="gl-responsive-table-row">
- <gl-skeleton-loading />
- </div>
- </template>
- <template v-else>
- <function-row v-for="f in functions" :key="f.name" :func="f" />
- </template>
- </div>
+ </template>
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
@@ -108,16 +101,3 @@ export default {
<empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
</section>
</template>
-
-<style>
-.top-area {
- border-bottom: 0;
-}
-
-.function-element {
- border-bottom: 1px solid #e5e5e5;
- border-bottom-color: rgb(229, 229, 229);
- border-bottom-style: solid;
- border-bottom-width: 1px;
-}
-</style>
diff --git a/app/assets/javascripts/serverless/components/pod_box.vue b/app/assets/javascripts/serverless/components/pod_box.vue
new file mode 100644
index 00000000000..04d3641bce3
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/pod_box.vue
@@ -0,0 +1,36 @@
+<script>
+export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ color: {
+ type: String,
+ required: false,
+ default: 'green',
+ },
+ },
+ methods: {
+ boxOffset(i) {
+ return 20 * (i - 1);
+ },
+ },
+};
+</script>
+
+<template>
+ <svg :width="boxOffset(count + 1)" :height="20">
+ <rect
+ v-for="i in count"
+ :key="i"
+ width="15"
+ height="15"
+ rx="5"
+ ry="5"
+ :fill="color"
+ :x="boxOffset(i)"
+ y="0"
+ />
+ </svg>
+</template>
diff --git a/app/assets/javascripts/serverless/components/url.vue b/app/assets/javascripts/serverless/components/url.vue
new file mode 100644
index 00000000000..ca53bf6c52a
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/url.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ ClipboardButton,
+ },
+ props: {
+ uri: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="clipboard-group">
+ <div class="url-text-field label label-monospace">{{ uri }}</div>
+ <clipboard-button
+ :text="uri"
+ :title="s__('ServerlessURL|Copy URL to clipboard')"
+ class="input-group-text js-clipboard-btn"
+ />
+ <gl-button
+ :href="uri"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="input-group-text btn btn-default"
+ >
+ <icon name="external-link" />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 3e3b81ba247..47a510d5fb5 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -4,23 +4,65 @@ import { s__ } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import ServerlessStore from './stores/serverless_store';
+import ServerlessDetailsStore from './stores/serverless_details_store';
import GetFunctionsService from './services/get_functions_service';
import Functions from './components/functions.vue';
+import FunctionDetails from './components/function_details.vue';
export default class Serverless {
constructor() {
- const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
- '.js-serverless-functions-page',
- ).dataset;
+ if (document.querySelector('.js-serverless-function-details-page') != null) {
+ const {
+ serviceName,
+ serviceDescription,
+ serviceEnvironment,
+ serviceUrl,
+ serviceNamespace,
+ servicePodcount,
+ } = document.querySelector('.js-serverless-function-details-page').dataset;
+ const el = document.querySelector('#js-serverless-function-details');
+ this.store = new ServerlessDetailsStore();
+ const { store } = this;
- this.service = new GetFunctionsService(statusPath);
- this.knativeInstalled = installed !== undefined;
- this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
- this.initServerless();
- this.functionLoadCount = 0;
+ const service = {
+ name: serviceName,
+ description: serviceDescription,
+ environment: serviceEnvironment,
+ url: serviceUrl,
+ namespace: serviceNamespace,
+ podcount: servicePodcount,
+ };
- if (statusPath && this.knativeInstalled) {
- this.initPolling();
+ this.store.updateDetailedFunction(service);
+ this.functionDetails = new Vue({
+ el,
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement(FunctionDetails, {
+ props: {
+ func: this.state.functionDetail,
+ },
+ });
+ },
+ });
+ } else {
+ const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ '.js-serverless-functions-page',
+ ).dataset;
+
+ this.service = new GetFunctionsService(statusPath);
+ this.knativeInstalled = installed !== undefined;
+ this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
+ this.initServerless();
+ this.functionLoadCount = 0;
+
+ if (statusPath && this.knativeInstalled) {
+ this.initPolling();
+ }
}
}
@@ -55,7 +97,7 @@ export default class Serverless {
resource: this.service,
method: 'fetchData',
successCallback: data => this.handleSuccess(data),
- errorCallback: () => this.handleError(),
+ errorCallback: () => Serverless.handleError(),
});
if (!Visibility.hidden()) {
@@ -64,7 +106,7 @@ export default class Serverless {
this.service
.fetchData()
.then(data => this.handleSuccess(data))
- .catch(() => this.handleError());
+ .catch(() => Serverless.handleError());
}
Visibility.change(() => {
@@ -102,5 +144,6 @@ export default class Serverless {
}
this.functions.$destroy();
+ this.functionDetails.$destroy();
}
}
diff --git a/app/assets/javascripts/serverless/stores/serverless_details_store.js b/app/assets/javascripts/serverless/stores/serverless_details_store.js
new file mode 100644
index 00000000000..5394d2cded1
--- /dev/null
+++ b/app/assets/javascripts/serverless/stores/serverless_details_store.js
@@ -0,0 +1,11 @@
+export default class ServerlessDetailsStore {
+ constructor() {
+ this.state = {
+ functionDetail: {},
+ };
+ }
+
+ updateDetailedFunction(func) {
+ this.state.functionDetail = func;
+ }
+}
diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js
index 774c15b5b12..816d55a03f9 100644
--- a/app/assets/javascripts/serverless/stores/serverless_store.js
+++ b/app/assets/javascripts/serverless/stores/serverless_store.js
@@ -1,7 +1,7 @@
export default class ServerlessStore {
constructor(knativeInstalled = false, clustersPath, helpPath) {
this.state = {
- functions: [],
+ functions: {},
hasFunctionData: true,
loadingData: true,
installed: knativeInstalled,
@@ -10,8 +10,13 @@ export default class ServerlessStore {
};
}
- updateFunctionsFromServer(functions = []) {
- this.state.functions = functions;
+ updateFunctionsFromServer(upstreamFunctions = []) {
+ this.state.functions = upstreamFunctions.reduce((rv, func) => {
+ const envs = rv;
+ envs[func.environment_scope] = (rv[func.environment_scope] || []).concat([func]);
+
+ return envs;
+ }, {});
}
updateLoadingState(loadingData) {
diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
index 14a89ef9293..3a8631a196f 100644
--- a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
+++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
@@ -12,9 +12,8 @@ class EmojiMenuInModal extends AwardsHandler {
this.bindEvents();
}
- postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
+ postEmoji($emojiButton, awardUrl, selectedEmoji) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
- callback();
}
}
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index f04f7606976..e9ed05e30cd 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -66,19 +66,23 @@ export default {
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
- if (this.emoji) {
- this.emojiTag = Emoji.glEmojiTag(this.emoji);
- }
- this.noEmoji = this.emoji === '';
- this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
+ Emoji.initEmojiMap()
+ .then(() => {
+ if (this.emoji) {
+ this.emojiTag = Emoji.glEmojiTag(this.emoji);
+ }
+ this.noEmoji = this.emoji === '';
+ this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
- this.emojiMenu = new EmojiMenuInModal(
- Emoji,
- toggleEmojiMenuButtonSelector,
- emojiMenuClass,
- this.setEmoji,
- this.$refs.userStatusForm,
- );
+ this.emojiMenu = new EmojiMenuInModal(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ emojiMenuClass,
+ this.setEmoji,
+ this.$refs.userStatusForm,
+ );
+ })
+ .catch(() => createFlash(__('Failed to load emoji list.')));
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
@@ -219,7 +223,7 @@ export default {
name="button"
type="button"
class="js-clear-user-status-button clear-user-status btn"
- @click="clearStatusInputs();"
+ @click="clearStatusInputs()"
>
<icon name="close" />
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index faea64c9841..c5cfa92f3c8 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -104,9 +104,7 @@ export default {
</div>
<div class="title hide-collapsed">
- {{
- sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName })
- }}
+ {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
class="float-right lock-edit"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index d3a4f9c81e0..c03b2a68c78 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -102,13 +102,13 @@ export default {
/>
<div class="title hide-collapsed">
{{ __('Time tracking') }}
- <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true);">
+ <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)">
<i class="fa fa-question-circle" aria-hidden="true"> </i>
</div>
<div
v-if="showHelpState"
class="close-help-button float-right"
- @click="toggleHelpState(false);"
+ @click="toggleHelpState(false)"
>
<i class="fa fa-close" aria-hidden="true"> </i>
</div>
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index edefb3735d7..5172a1ef3d6 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import 'deckar01-task_list';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
@@ -8,46 +9,79 @@ export default class TaskList {
this.selector = options.selector;
this.dataType = options.dataType;
this.fieldName = options.fieldName;
+ this.lockVersion = options.lockVersion;
+ this.taskListContainerSelector = `${this.selector} .js-task-list-container`;
+ this.updateHandler = this.update.bind(this);
this.onSuccess = options.onSuccess || (() => {});
- this.onError = function showFlash(e) {
- let errorMessages = '';
+ this.onError =
+ options.onError ||
+ function showFlash(e) {
+ let errorMessages = '';
- if (e.response.data && typeof e.response.data === 'object') {
- errorMessages = e.response.data.errors.join(' ');
- }
+ if (e.response.data && typeof e.response.data === 'object') {
+ errorMessages = e.response.data.errors.join(' ');
+ }
- return new Flash(errorMessages || 'Update failed', 'alert');
- };
+ return new Flash(errorMessages || __('Update failed'), 'alert');
+ };
this.init();
}
init() {
- // Prevent duplicate event bindings
- this.disable();
- $(`${this.selector} .js-task-list-container`).taskList('enable');
- $(document).on(
- 'tasklist:changed',
- `${this.selector} .js-task-list-container`,
- this.update.bind(this),
- );
+ this.disable(); // Prevent duplicate event bindings
+
+ $(this.taskListContainerSelector).taskList('enable');
+ $(document).on('tasklist:changed', this.taskListContainerSelector, this.updateHandler);
+ }
+
+ getTaskListTarget(e) {
+ return e && e.currentTarget ? $(e.currentTarget) : $(this.taskListContainerSelector);
+ }
+
+ disableTaskListItems(e) {
+ this.getTaskListTarget(e).taskList('disable');
+ }
+
+ enableTaskListItems(e) {
+ this.getTaskListTarget(e).taskList('enable');
}
disable() {
- $(`${this.selector} .js-task-list-container`).taskList('disable');
- $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`);
+ this.disableTaskListItems();
+ $(document).off('tasklist:changed', this.taskListContainerSelector);
}
update(e) {
const $target = $(e.target);
+ const { index, checked, lineNumber, lineSource } = e.detail;
const patchData = {};
+
patchData[this.dataType] = {
[this.fieldName]: $target.val(),
+ lock_version: this.lockVersion,
+ update_task: {
+ index,
+ checked,
+ line_number: lineNumber,
+ line_source: lineSource,
+ },
};
+ this.disableTaskListItems(e);
+
return axios
.patch($target.data('updateUrl') || $('form.js-issuable-update').attr('action'), patchData)
- .then(({ data }) => this.onSuccess(data))
- .catch(err => this.onError(err));
+ .then(({ data }) => {
+ this.lockVersion = data.lock_version;
+ this.enableTaskListItems(e);
+
+ return this.onSuccess(data);
+ })
+ .catch(({ response }) => {
+ this.enableTaskListItems(e);
+
+ return this.onError(response.data);
+ });
}
}
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index 560f50ebf8f..e5dd7a465ea 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -2,11 +2,13 @@ import _ from 'underscore';
import $ from 'jquery';
import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
+import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
const SCROLL_MARGIN = 5;
Terminal.applyAddon(fit);
+Terminal.applyAddon(webLinks);
export default class GLTerminal {
constructor(element, options = {}) {
@@ -48,6 +50,7 @@ export default class GLTerminal {
this.terminal.open(this.container);
this.terminal.fit();
+ this.terminal.webLinksInit();
this.terminal.focus();
this.socket.onopen = () => {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index ce051582299..8c71615dff2 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -93,23 +93,22 @@ function UsersSelect(currentUser, els, options = {}) {
}
// Save current selected user to the DOM
- const input = document.createElement('input');
- input.type = 'hidden';
- input.name = $dropdown.data('fieldName');
-
- const currentUserInfo = $dropdown.data('currentUserInfo');
-
- if (currentUserInfo) {
- input.value = currentUserInfo.id;
- input.dataset.meta = _.escape(currentUserInfo.name);
- } else if (_this.currentUser) {
- input.value = _this.currentUser.id;
- }
+ const currentUserInfo = $dropdown.data('currentUserInfo') || {};
+ const currentUser = _this.currentUser || {};
+ const fieldName = $dropdown.data('fieldName');
+ const userName = currentUserInfo.name;
+ const userId = currentUserInfo.id || currentUser.id;
+
+ const inputHtmlString = _.template(`
+ <input type="hidden" name="<%- fieldName %>"
+ data-meta="<%- userName %>"
+ value="<%- userId %>" />
+ `)({ fieldName, userName, userId });
if ($selectbox) {
- $dropdown.parent().before(input);
+ $dropdown.parent().before(inputHtmlString);
} else {
- $dropdown.after(input);
+ $dropdown.after(inputHtmlString);
}
};
@@ -579,101 +578,109 @@ function UsersSelect(currentUser, els, options = {}) {
};
})(this),
);
- $('.ajax-users-select').each(
- (function(_this) {
- return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('projectId');
- options.groupId = $(select).data('groupId');
- options.showCurrentUser = $(select).data('currentUser');
- options.authorId = $(select).data('authorId');
- options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: 'Search for a user',
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- ref = data.results;
-
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each(
+ (function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('projectId');
+ options.groupId = $(select).data('groupId');
+ options.showCurrentUser = $(select).data('currentUser');
+ options.authorId = $(select).data('authorId');
+ options.skipUsers = $(select).data('skipUsers');
+ showNullUser = $(select).data('nullUser');
+ showAnyUser = $(select).data('anyUser');
+ showEmailUser = $(select).data('emailUser');
+ firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: 'Search for a user',
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
+ data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ ref = data.results;
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ name: name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- nullUser = {
- name: 'Unassigned',
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ var trimmed = query.term.trim();
+ emailUser = {
+ name: 'Invite "' + trimmed + '" by email',
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- anyUser = {
- name: name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- var trimmed = query.term.trim();
- emailUser = {
- name: 'Invite "' + trimmed + '" by email',
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ 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-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ },
});
- },
- initSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.initSelection.apply(_this, args);
- },
- 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-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
- },
- });
- };
- })(this),
- );
+ };
+ })(this),
+ );
+ })
+ .catch(() => {});
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 2f2a37347af..da0a9483f8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -54,6 +54,12 @@ export default {
deployTimeago() {
return this.timeFormated(this.deployment.deployed_at);
},
+ deploymentExternalUrl() {
+ if (this.deployment.changes && this.deployment.changes.length === 1) {
+ return this.deployment.changes[0].external_url;
+ }
+ return this.deployment.external_url;
+ },
hasExternalUrls() {
return !!(this.deployment.external_url && this.deployment.external_url_formatted);
},
@@ -78,7 +84,7 @@ export default {
: '';
},
shouldRenderDropdown() {
- return this.deployment.changes && this.deployment.changes.length > 0;
+ return this.deployment.changes && this.deployment.changes.length > 1;
},
showMemoryUsage() {
return this.hasMetrics && this.showMetrics;
@@ -154,12 +160,12 @@ export default {
v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
- :main-action-link="deployment.external_url"
+ :main-action-link="deploymentExternalUrl"
filter-key="path"
>
<template slot="mainAction" slot-scope="slotProps">
<review-app-link
- :link="deployment.external_url"
+ :link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
</template>
@@ -183,7 +189,7 @@ export default {
</filtered-search-dropdown>
<review-app-link
v-else
- :link="deployment.external_url"
+ :link="deploymentExternalUrl"
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
/>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3b9fc2661ef..50ab7ead582 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -135,7 +135,7 @@ export default {
<span class="dropdown">
<button
type="button"
- class="btn dropdown-toggle"
+ class="btn dropdown-toggle qa-dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
@@ -145,12 +145,20 @@ export default {
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
- <a :href="mr.emailPatchesPath" class="js-download-email-patches" download>
+ <a
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches qa-download-email-patches"
+ download
+ >
{{ s__('mrWidget|Email patches') }}
</a>
</li>
<li>
- <a :href="mr.plainDiffPath" class="js-download-plain-diff" download>
+ <a
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff qa-download-plain-diff"
+ download
+ >
{{ s__('mrWidget|Plain diff') }}
</a>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index f11cf21b0ca..9e63aa00341 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -5,6 +5,7 @@ import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
export default {
name: 'MRWidgetPipeline',
@@ -13,7 +14,10 @@ export default {
CiIcon,
Icon,
TooltipOnTruncate,
+ LinkedPipelinesMiniList: () =>
+ import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
+ mixins: [mrWidgetPipelineMixin],
props: {
pipeline: {
type: Object,
@@ -82,8 +86,7 @@ export default {
<div v-if="hasPipeline || hasCIError" class="ci-widget media">
<template v-if="hasCIError">
<div
- class="add-border ci-status-icon ci-status-icon-failed ci-error
- js-ci-error append-right-default"
+ class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default"
>
<icon :size="32" name="status_failed_borderless" />
</div>
@@ -101,16 +104,13 @@ export default {
<a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</a
>
-
{{ pipeline.details.status.label }}
-
<template v-if="hasCommitInfo">
for
<a
:href="pipeline.commit.commit_path"
class="commit-sha js-commit-link font-weight-normal"
- >
- {{ pipeline.commit.short_id }}</a
+ >{{ pipeline.commit.short_id }}</a
>
on
<tooltip-on-truncate
@@ -126,15 +126,22 @@ export default {
</div>
<div>
<span class="mr-widget-pipeline-graph">
- <span v-if="hasStages" class="stage-cell">
- <div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
- >
- <pipeline-stage :stage="stage" />
- </div>
+ <span class="stage-cell">
+ <linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" />
+ <template v-if="hasStages">
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ :class="{
+ 'has-downstream': hasDownstream(i),
+ }"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
+ </template>
</span>
+ <linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index dd940548e30..780ecdcdac4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -7,7 +7,7 @@ export default {
tooltip,
},
created() {
- this.removesBranchText = __('<strong>Removes</strong> source branch');
+ this.removesBranchText = __('<strong>Deletes</strong> source branch');
this.tooltipTitle = __('A user with write access to the source branch selected this option');
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
new file mode 100644
index 00000000000..a38f25cce35
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -0,0 +1,40 @@
+<script>
+export default {
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <div class="commit-message-editor">
+ <div class="d-flex flex-wrap align-items-center justify-content-between">
+ <label class="col-form-label" :for="inputId">
+ <strong>{{ label }}</strong>
+ </label>
+ <slot name="header"></slot>
+ </div>
+ <textarea
+ :id="inputId"
+ :value="value"
+ class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ required="required"
+ rows="7"
+ @input="$emit('input', $event.target.value)"
+ ></textarea>
+ <slot name="checkbox"></slot>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
new file mode 100644
index 00000000000..b6722de5277
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ commits: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown
+ right
+ text="Use an existing commit message"
+ variant="link"
+ class="mr-commit-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="commit in commits"
+ :key="commit.short_id"
+ class="text-nowrap text-truncate"
+ @click="$emit('input', commit.message)"
+ >
+ <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
new file mode 100644
index 00000000000..33963d5e1e6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import _ from 'underscore';
+import { __, n__, sprintf, s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ isSquashEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ commitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.expanded ? 'chevron-down' : 'chevron-right';
+ },
+ commitsCountMessage() {
+ return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount);
+ },
+ modifyLinkMessage() {
+ return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit');
+ },
+ ariaLabel() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ message() {
+ return sprintf(
+ s__(
+ 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
+ ),
+ {
+ commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
+ mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
+ targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`,
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ toggle() {
+ this.expanded = !this.expanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2"
+ @click="toggle()"
+ >
+ <gl-button
+ :aria-label="ariaLabel"
+ variant="blank"
+ class="commit-edit-toggle square s24 mr-2"
+ @click.stop="toggle()"
+ >
+ <icon :name="collapseIcon" :size="16" />
+ </gl-button>
+ <span v-if="expanded">{{ __('Collapse') }}</span>
+ <span v-else>
+ <span class="vertical-align-middle" v-html="message"></span>
+ <gl-button variant="link" class="modify-message-button">
+ {{ modifyLinkMessage }}
+ </gl-button>
+ </span>
+ </div>
+ <div v-show="expanded"><slot></slot></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 27352e0b2b1..f6f445c1cef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,10 +1,14 @@
<script>
-import statusIcon from '../mr_widget_status_icon.vue';
+import $ from 'jquery';
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
+import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetConflicts',
components: {
- statusIcon,
+ StatusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
@@ -15,6 +19,52 @@ export default {
default: () => ({}),
},
},
+ computed: {
+ popoverTitle() {
+ return s__(
+ 'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
+ );
+ },
+ showResolveButton() {
+ return this.mr.conflictResolutionPath && this.mr.canMerge;
+ },
+ showPopover() {
+ return this.showResolveButton && this.mr.sourceBranchProtected;
+ },
+ },
+ mounted() {
+ if (this.showPopover) {
+ const $el = $(this.$refs.popover);
+
+ $el
+ .popover({
+ html: true,
+ trigger: 'focus',
+ container: 'body',
+ placement: 'top',
+ template:
+ '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
+ title: s__(
+ 'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
+ ),
+ content: sprintf(
+ s__('mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}'),
+ {
+ link_start: `<a href="${_.escape(
+ this.mr.conflictsDocsPath,
+ )}" target="_blank" rel="noopener noreferrer">`,
+ link_end: '</a>',
+ },
+ false,
+ ),
+ })
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', debouncedMouseleave(300))
+ .on('show.bs.popover', () => {
+ window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
+ });
+ }
+ },
};
</script>
<template>
@@ -38,13 +88,15 @@ To merge this request, first rebase locally.`)
}}
</span>
</span>
- <a
- v-if="mr.canMerge && mr.conflictResolutionPath"
- :href="mr.conflictResolutionPath"
- class="js-resolve-conflicts-button btn btn-default btn-sm"
- >
- {{ s__('mrWidget|Resolve conflicts') }}
- </a>
+ <span v-if="showResolveButton" ref="popover">
+ <a
+ :href="mr.conflictResolutionPath"
+ :disabled="mr.sourceBranchProtected"
+ class="js-resolve-conflicts-button btn btn-default btn-sm"
+ >
+ {{ s__('mrWidget|Resolve conflicts') }}
+ </a>
+ </span>
<button
v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-sm"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 02c76db4a50..1b3af2fccf2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -106,11 +106,11 @@ export default {
<a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
- {{ s__('mrWidget|The source branch will be removed') }}
+ {{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
<span class="append-right-10">
- {{ s__('mrWidget|The source branch will not be removed') }}
+ {{ s__('mrWidget|The source branch will not be deleted') }}
</span>
<a
v-if="canRemoveSourceBranch"
@@ -121,7 +121,7 @@ export default {
@click.prevent="removeSourceBranch"
>
<i v-if="isRemovingSourceBranch" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
- {{ s__('mrWidget|Remove source branch') }}
+ {{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index fe83fe58b67..b9562fbc260 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -84,7 +84,7 @@ export default {
.removeSourceBranch()
.then(res => res.data)
.then(data => {
- if (data.message === 'Branch was removed') {
+ if (data.message === 'Branch was deleted') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
@@ -174,22 +174,22 @@ export default {
</template>
</p>
<p v-if="mr.sourceBranchRemoved">
- {{ s__('mrWidget|The source branch has been removed') }}
+ {{ s__('mrWidget|The source branch has been deleted') }}
</p>
<p v-if="shouldShowRemoveSourceBranch" class="space-children">
- <span>{{ s__('mrWidget|You can remove source branch now') }}</span>
+ <span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
<button
:disabled="isMakingRequest"
type="button"
class="btn btn-sm btn-default js-remove-branch-button"
@click="removeSourceBranch"
>
- {{ s__('mrWidget|Remove Source Branch') }}
+ {{ s__('mrWidget|Delete source branch') }}
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<gl-loading-icon :inline="true" />
- <span> {{ s__('mrWidget|The source branch is being removed') }} </span>
+ <span> {{ s__('mrWidget|The source branch is being deleted') }} </span>
</p>
</section>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 84c8a3464a5..9b4e80ee3a3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -2,17 +2,24 @@
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import { __ } from '~/locale';
import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
+import CommitsHeader from './commits_header.vue';
+import CommitEdit from './commit_edit.vue';
+import CommitMessageDropdown from './commit_message_dropdown.vue';
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
- 'squash-before-merge': SquashBeforeMerge,
+ SquashBeforeMerge,
+ CommitsHeader,
+ CommitEdit,
+ CommitMessageDropdown,
},
props: {
mr: { type: Object, required: true },
@@ -22,26 +29,20 @@ export default {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
- useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
- showCommitMessageEditor: false,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
+ squashBeforeMerge: this.mr.squash,
successSvg,
warningSvg,
+ squashCommitMessage: this.mr.squashCommitMessage,
};
},
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
},
- commitMessageLinkTitle() {
- const withDesc = 'Include description in commit message';
- const withoutDesc = "Don't include description in commit message";
-
- return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
- },
status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
@@ -83,9 +84,9 @@ export default {
},
mergeButtonText() {
if (this.isMergingImmediately) {
- return 'Merge in progress';
+ return __('Merge in progress');
} else if (this.shouldShowMergeWhenPipelineSucceedsText) {
- return 'Merge when pipeline succeeds';
+ return __('Merge when pipeline succeeds');
}
return 'Merge';
@@ -97,7 +98,7 @@ export default {
const { commitMessage } = this;
return Boolean(
!commitMessage.length ||
- !this.shouldShowMergeControls() ||
+ !this.shouldShowMergeControls ||
this.isMakingRequest ||
this.mr.preventMerge,
);
@@ -109,24 +110,14 @@ export default {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
},
- },
- created() {
- eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash);
- },
- beforeDestroy() {
- eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash);
- },
- methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
- updateCommitMessage() {
- const cmwd = this.mr.commitMessageWithDescription;
- this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
- this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
- },
- toggleCommitMessageEditor() {
- this.showCommitMessageEditor = !this.showCommitMessageEditor;
+ },
+ methods: {
+ updateMergeCommitMessage(includeDescription) {
+ const { commitMessageWithDescription, commitMessage } = this.mr;
+ this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
// TODO: Remove no-param-reassign
@@ -143,7 +134,8 @@ export default {
commit_message: this.commitMessage,
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
- squash: this.mr.squash,
+ squash: this.squashBeforeMerge,
+ squash_commit_message: this.squashCommitMessage,
};
this.isMakingRequest = true;
@@ -163,12 +155,9 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
});
},
- handleUpdateSquash(val) {
- this.mr.squash = val;
- },
initiateMergePolling() {
simplePoll((continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
@@ -202,7 +191,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
});
},
initiateRemoveSourceBranchPolling() {
@@ -231,7 +220,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while removing the source branch. Please try again.'); // eslint-disable-line
+ new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
});
},
},
@@ -239,126 +228,136 @@ export default {
</script>
<template>
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
- <div class="media-body">
- <div class="mr-widget-body-controls media space-children">
- <span class="btn-group">
- <button
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button"
- class="qa-merge-button"
- @click="handleMergeButtonClick();"
- >
- <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
- {{ mergeButtonText }}
- </button>
- <button
- v-if="shouldShowMergeOptionsDropdown"
- :disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
- data-toggle="dropdown"
- aria-label="Select merge moment"
- >
- <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
- </button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- >
- <li>
- <a
- class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
- href="#"
- @click.prevent="handleMergeButtonClick(true);"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
- <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
- </span>
- </a>
- </li>
- <li>
- <a
- class="accept-merge-request qa-merge-immediately-option"
- href="#"
- @click.prevent="handleMergeButtonClick(false, true);"
- >
- <span class="media">
- <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
- <span class="media-body merge-opt-title">Merge immediately</span>
- </span>
- </a>
- </li>
- </ul>
- </span>
- <div class="media-body-wrap space-children">
- <template v-if="shouldShowMergeControls()">
- <label v-if="mr.canRemoveSourceBranch">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox"
- type="checkbox"
- />
- Remove source branch
- </label>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled"
- />
-
- <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message">
- Fast-forward merge without a merge commit
- </span>
+ <div>
+ <div class="mr-widget-body media">
+ <status-icon :status="iconClass" />
+ <div class="media-body">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group">
<button
- v-else
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-sm"
+ :class="mergeButtonClass"
type="button"
- @click="toggleCommitMessageEditor"
+ class="qa-merge-button"
+ @click="handleMergeButtonClick()"
>
- Modify commit message
+ <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ {{ mergeButtonText }}
</button>
- </template>
- <template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- You can only merge once the items above are resolved
- </span>
- </template>
- </div>
- </div>
- <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor">
- <div class="form-group clearfix">
- <label class="col-form-label" for="commit-message"> Commit message </label>
- <div class="col-sm-10">
- <div class="commit-message-container">
- <div class="max-width-marker"></div>
- <textarea
- id="commit-message"
- v-model="commitMessage"
- class="form-control js-commit-message"
- required="required"
- rows="14"
- name="Commit message"
- ></textarea>
- </div>
- <p class="hint">
- Try to keep the first line under 52 characters and the others under 72
- </p>
- <div class="hint">
- <a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a>
- </div>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment"
+ >
+ <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ >
+ <li>
+ <a
+ class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
+ <span class="media-body merge-opt-title">{{
+ __('Merge when pipeline succeeds')
+ }}</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ class="accept-merge-request qa-merge-immediately-option"
+ href="#"
+ @click.prevent="handleMergeButtonClick(false, true)"
+ >
+ <span class="media">
+ <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
+ <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
+ </span>
+ </a>
+ </li>
+ </ul>
+ </span>
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls">
+ <label v-if="mr.canRemoveSourceBranch">
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox"
+ type="checkbox"
+ />
+ {{ __('Delete source branch') }}
+ </label>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ v-model="squashBeforeMerge"
+ :help-path="mr.squashBeforeMergeHelpPath"
+ :is-disabled="isMergeButtonDisabled"
+ />
+ </template>
+ <template v-else>
+ <span class="bold js-resolve-mr-widget-items-message">
+ {{ __('You can only merge once the items above are resolved') }}
+ </span>
+ </template>
</div>
</div>
</div>
</div>
+ <template v-if="shouldShowMergeControls">
+ <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
+ {{ __('Fast-forward merge without a merge commit') }}
+ </div>
+ <template v-else>
+ <commits-header
+ :is-squash-enabled="squashBeforeMerge"
+ :commits-count="mr.commitsCount"
+ :target-branch="mr.targetBranch"
+ >
+ <ul class="border-top content-list commits-list flex-list">
+ <commit-edit
+ v-if="squashBeforeMerge && shouldShowSquashBeforeMerge"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ squash
+ >
+ <commit-message-dropdown
+ slot="header"
+ v-model="squashCommitMessage"
+ :commits="mr.commits"
+ />
+ </commit-edit>
+ <commit-edit
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ >
+ <label slot="checkbox">
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
+ />
+ {{ __('Include merge request description') }}
+ </label>
+ </commit-edit>
+ </ul>
+ </commits-header>
+ </template>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index e71acf0d7dd..b1f5655a15a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,6 +1,5 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import eventHub from '~/vue_merge_request_widget/event_hub';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -11,23 +10,19 @@ export default {
tooltip,
},
props: {
- mr: {
- type: Object,
- required: true,
- },
- isMergeButtonDisabled: {
+ value: {
type: Boolean,
required: true,
},
- },
- data() {
- return {
- squashBeforeMerge: this.mr.squash,
- };
- },
- methods: {
- updateSquashModel() {
- eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge);
+ helpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
},
},
};
@@ -37,18 +32,19 @@ export default {
<div class="accept-control inline">
<label class="merge-param-checkbox">
<input
- v-model="squashBeforeMerge"
- :disabled="isMergeButtonDisabled"
+ :checked="value"
+ :disabled="isDisabled"
type="checkbox"
name="squash"
class="qa-squash-checkbox"
- @change="updateSquashModel"
+ @change="$emit('input', $event.target.checked)"
/>
{{ __('Squash commits') }}
</label>
<a
+ v-if="helpPath"
v-tooltip
- :href="mr.squashBeforeMergeHelpPath"
+ :href="helpPath"
data-title="About this feature"
data-placement="bottom"
target="_blank"
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
deleted file mode 100644
index 8780aa4bd1c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetOptions from './mr_widget_options.vue';
-
-export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 60cebbfc2b2..0cedbdbdfef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import MrWidgetOptions from './ee_switch_mr_widget_options';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/mixins/mr_widget_pipeline.js
new file mode 100644
index 00000000000..96e8bb45e34
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/mr_widget_pipeline.js
@@ -0,0 +1,15 @@
+export default {
+ computed: {
+ triggered() {
+ return [];
+ },
+ triggeredBy() {
+ return [];
+ },
+ },
+ methods: {
+ hasDownstream() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index d8a75388e84..57c4dfbe3b7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -3,6 +3,9 @@ import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
+import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
+import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
@@ -28,11 +31,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
-import MRWidgetStore from './stores/ee_switch_mr_widget_store';
-import MRWidgetService from './services/ee_switch_mr_widget_service';
import eventHub from './event_hub';
-import stateMaps from './stores/ee_switch_state_maps';
-import SquashBeforeMerge from './components/states/squash_before_merge.vue';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
@@ -59,7 +58,6 @@ export default {
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
'sha-mismatch': ShaMismatchState,
- 'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState,
@@ -106,6 +104,9 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
+ shouldRenderCollaborationStatus() {
+ return this.mr.allowCollaboration && this.mr.isOpen;
+ },
shouldRenderMergedPipeline() {
return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
},
@@ -141,8 +142,8 @@ export default {
}
},
methods: {
- createService(store) {
- const endpoints = {
+ getServiceEndpoints(store) {
+ return {
mergePath: store.mergePath,
mergeCheckPath: store.mergeCheckPath,
cancelAutoMergePath: store.cancelAutoMergePath,
@@ -153,7 +154,9 @@ export default {
mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath,
};
- return new MRWidgetService(endpoints);
+ },
+ createService(store) {
+ return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
return this.service
@@ -315,7 +318,7 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
- <section v-if="mr.allowCollaboration" class="mr-info-list mr-links">
+ <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
{{ s__('mrWidget|Allows commits from members who can merge to the target branch') }}
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
deleted file mode 100644
index ea2aabb78fe..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetService from './mr_widget_service';
-
-export default MRWidgetService;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
deleted file mode 100644
index ebef30e3eab..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import getStateKey from './get_state_key';
-
-export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
deleted file mode 100644
index 92a07c53f2d..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MergeRequestStore from './mr_widget_store';
-
-export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
deleted file mode 100644
index 50cf9503ea7..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import stateMaps from './state_maps';
-
-export default stateMaps;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 066a3b833d7..0cc4fd59f5e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -13,6 +13,8 @@ export default function deviseState(data) {
return stateKey.conflicts;
} else if (data.work_in_progress) {
return stateKey.workInProgress;
+ } else if (this.shouldBeRebased) {
+ return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) {
@@ -25,8 +27,6 @@ export default function deviseState(data) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
- } else if (this.shouldBeRebased) {
- return stateKey.rebase;
} else if (this.canBeMerged) {
return stateKey.readyToMerge;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index c777bcca0fa..58363f632a9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import getStateKey from './ee_switch_get_state_key';
+import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
@@ -29,17 +29,21 @@ export default class MergeRequestStore {
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
+ this.sourceBranchProtected = data.source_branch_protected;
+ this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeStatus = data.merge_status;
- this.commitMessage = data.merge_commit_message;
+ this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.mergeCommitSha = data.merge_commit_sha;
- this.commitMessageWithDescription = data.merge_commit_message_with_description;
+ this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
+ this.commits = data.commits_without_merge_commits || [];
+ this.squashCommitMessage = data.default_squash_commit_message;
this.initRebase(data);
if (data.issues_links) {
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
index 4abf795f7bd..eabf5d4bf60 100644
--- a/app/assets/javascripts/vue_shared/components/bar_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -293,8 +293,8 @@ export default {
:title="setTooltipTitle(data)"
class="bar-rect"
data-placement="top"
- @mouseover="barHoveredIn(index);"
- @mouseout="barHoveredOut(index);"
+ @mouseover="barHoveredIn(index)"
+ @mouseout="barHoveredOut(index)"
/>
</template>
</g>
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
index ddbb14ae812..56bafebf4ce 100644
--- a/app/assets/javascripts/vue_shared/components/callout.vue
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -11,13 +11,14 @@ export default {
},
message: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
};
</script>
<template>
<div :class="`bs-callout bs-callout-${category}`" role="alert" aria-live="assertive">
- {{ message }}
+ {{ message }} <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index bb7710f708e..e9ab6f5ba7a 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -37,6 +37,11 @@ export default {
required: false,
default: 12,
},
+ isCentered: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
changedIcon() {
@@ -78,7 +83,12 @@ export default {
</script>
<template>
- <span v-gl-tooltip.right :title="tooltipTitle" class="file-changed-icon ml-auto">
+ <span
+ v-gl-tooltip.right
+ :title="tooltipTitle"
+ :class="{ 'ml-auto': isCentered }"
+ class="file-changed-icon"
+ >
<icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index b8eb555106f..e6f0a1c69cd 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
+ * - Linked pipelines
*/
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
@@ -46,6 +47,11 @@ export default {
required: false,
default: false,
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
cssClass() {
@@ -59,5 +65,5 @@ export default {
};
</script>
<template>
- <span :class="cssClass"> <icon :name="icon" :size="size" /> </span>
+ <span :class="cssClass"> <icon :name="icon" :size="size" :css-classes="cssClasses" /> </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index 2129f90d497..36b3ee05456 100644
--- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -95,7 +95,7 @@ export default {
class="close float-right"
data-dismiss="modal"
aria-label="Close"
- @click="emitCancel($event);"
+ @click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
@@ -112,7 +112,7 @@ export default {
type="button"
class="btn"
data-dismiss="modal"
- @click="emitCancel($event);"
+ @click="emitCancel($event)"
>
{{ closeButtonLabel }}
</button>
@@ -130,7 +130,7 @@ export default {
type="button"
class="btn js-primary-button"
data-dismiss="modal"
- @click="emitSubmit($event);"
+ @click="emitSubmit($event)"
>
{{ primaryButtonLabel }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 75c66ed850b..ebb253ff422 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -1,6 +1,5 @@
<script>
-import { diffModes } from '~/ide/constants';
-import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
+import { diffViewerModes, diffModes } from '~/ide/constants';
import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
import RenamedFile from './viewers/renamed.vue';
@@ -12,6 +11,10 @@ export default {
type: String,
required: true,
},
+ diffViewerMode: {
+ type: String,
+ required: true,
+ },
newPath: {
type: String,
required: true,
@@ -46,7 +49,7 @@ export default {
},
computed: {
viewer() {
- if (this.diffMode === diffModes.renamed) {
+ if (this.diffViewerMode === diffViewerModes.renamed) {
return RenamedFile;
} else if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
@@ -54,11 +57,8 @@ export default {
if (!this.newPath) return null;
- const previewInfo = viewerInformationForPath(this.newPath);
- if (!previewInfo) return DownloadDiffViewer;
-
- switch (previewInfo.id) {
- case 'image':
+ switch (this.diffViewerMode) {
+ case diffViewerModes.image:
return ImageDiffViewer;
default:
return DownloadDiffViewer;
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index d5fda7e4ed3..cab92297ca7 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -75,7 +75,7 @@ export default {
:class="{
active: mode === $options.imageViewMode.twoup,
}"
- @click="changeMode($options.imageViewMode.twoup);"
+ @click="changeMode($options.imageViewMode.twoup)"
>
{{ s__('ImageDiffViewer|2-up') }}
</li>
@@ -83,7 +83,7 @@ export default {
:class="{
active: mode === $options.imageViewMode.swipe,
}"
- @click="changeMode($options.imageViewMode.swipe);"
+ @click="changeMode($options.imageViewMode.swipe)"
>
{{ s__('ImageDiffViewer|Swipe') }}
</li>
@@ -91,7 +91,7 @@ export default {
:class="{
active: mode === $options.imageViewMode.onion,
}"
- @click="changeMode($options.imageViewMode.onion);"
+ @click="changeMode($options.imageViewMode.onion)"
>
{{ s__('ImageDiffViewer|Onion skin') }}
</li>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue
new file mode 100644
index 00000000000..c5cdddf2f64
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue
@@ -0,0 +1,5 @@
+<template>
+ <div class="nothing-here-block">
+ {{ __('No preview for this file type') }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue
new file mode 100644
index 00000000000..d4d3038f066
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue
@@ -0,0 +1,5 @@
+<template>
+ <div class="nothing-here-block">
+ {{ __('This diff was suppressed by a .gitattributes entry.') }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/empty_component.js b/app/assets/javascripts/vue_shared/components/empty_component.js
new file mode 100644
index 00000000000..e4402020096
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/empty_component.js
@@ -0,0 +1,3 @@
+export default {
+ render: () => null,
+};
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
new file mode 100644
index 00000000000..b57455adaad
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -0,0 +1,299 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import Mousetrap from 'mousetrap';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+const originalStopCallback = Mousetrap.stopCallback;
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
+ },
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ showDiffStats: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ clearSearchOnClose: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ focusedIndex: -1,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus.filter(this.files, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ });
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ visible() {
+ this.$nextTick(() => {
+ if (!this.visible) {
+ if (this.clearSearchOnClose) {
+ this.searchText = '';
+ }
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = -1;
+
+ this.$nextTick(() => {
+ this.focusedIndex = 0;
+ });
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ mounted() {
+ if (this.files.length) {
+ this.focusedIndex = 0;
+ }
+
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggle(!this.visible);
+ });
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ toggle(visible) {
+ this.$emit('toggle', visible);
+ },
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggle(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggle(false);
+ this.$emit('click', file);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ mousetrapStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="file-finder-overlay" @mousedown.self="toggle(false)">
+ <div class="dropdown-menu diff-file-changes file-finder show">
+ <div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
+ <input
+ ref="searchInput"
+ v-model="searchText"
+ :placeholder="__('Search files')"
+ type="search"
+ class="dropdown-input-field"
+ autocomplete="off"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ :class="{
+ hidden: showClearInputButton,
+ }"
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ ></i>
+ <i
+ :aria-label="__('Clear search input')"
+ role="button"
+ class="fa fa-times dropdown-input-clear"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
+ <template v-if="filteredBlobsLength">
+ <li v-for="(file, index) in filteredBlobs" :key="file.key">
+ <item
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ :show-diff-stats="showDiffStats"
+ class="disable-hover"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li v-else class="dropdown-menu-empty-item">
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 200;
+}
+
+.file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.diff-file-changes {
+ top: 50px;
+ max-height: 327px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
new file mode 100644
index 00000000000..73511879ff2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -0,0 +1,126 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ Icon,
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ showDiffStats: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const { path } = this.file;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ :class="{
+ 'is-focused': focused,
+ }"
+ type="button"
+ class="diff-changed-file"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong class="diff-changed-file-name">
+ <span
+ v-for="(char, charIndex) in file.name.split('')"
+ :key="charIndex + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(charIndex) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span class="diff-changed-file-path prepend-top-5">
+ <span
+ v-for="(char, charIndex) in pathWithEllipsis.split('')"
+ :key="charIndex + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(charIndex) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
+ <span v-if="showDiffStats">
+ <span class="cgreen bold">
+ <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
+ </span>
+ <span class="cred bold ml-1">
+ <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
+ </span>
+ </span>
+ <changed-file-icon v-else :file="file" />
+ </span>
+ </button>
+</template>
+
+<style scoped>
+.highlighted {
+ color: #1f78d1;
+ font-weight: 600;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 9e713673678..0cbcdbf2eb4 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,11 +1,13 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
+import FileHeader from '~/vue_shared/components/file_row_header.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
name: 'FileRow',
components: {
+ FileHeader,
FileIcon,
Icon,
ChangedFileIcon,
@@ -34,21 +36,10 @@ export default {
required: false,
default: false,
},
- displayTextKey: {
- type: String,
- required: false,
- default: 'name',
- },
- shouldTruncateStart: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
mouseOver: false,
- truncateStart: 0,
};
},
computed: {
@@ -60,7 +51,7 @@ export default {
},
levelIndentation() {
return {
- marginLeft: `${this.level * 16}px`,
+ marginLeft: this.level ? `${this.level * 16}px` : null,
};
},
fileClass() {
@@ -71,14 +62,8 @@ export default {
'is-open': this.file.opened,
};
},
- outputText() {
- const text = this.file[this.displayTextKey];
-
- if (this.truncateStart === 0) {
- return text;
- }
-
- return `...${text.substring(this.truncateStart, text.length)}`;
+ childFilesLevel() {
+ return this.file.isHeader ? 0 : this.level + 1;
},
},
watch: {
@@ -92,15 +77,6 @@ export default {
if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true);
}
-
- if (this.shouldTruncateStart) {
- const { scrollWidth, offsetWidth } = this.$refs.textOutput;
- const textOverflow = scrollWidth - offsetWidth;
-
- if (textOverflow > 0) {
- this.truncateStart = Math.ceil(textOverflow / 5) + 3;
- }
- }
},
methods: {
toggleTreeOpen(path) {
@@ -156,13 +132,16 @@ export default {
<template>
<div>
+ <file-header v-if="file.isHeader" :path="file.path" />
<div
+ v-else
:class="fileClass"
+ :title="file.name"
class="file-row"
role="button"
@click="clickFile"
- @mouseover="toggleHover(true);"
- @mouseout="toggleHover(false);"
+ @mouseover="toggleHover(true)"
+ @mouseout="toggleHover(false)"
>
<div class="file-row-name-container">
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
@@ -175,7 +154,7 @@ export default {
:size="16"
/>
<changed-file-icon v-else :file="file" :size="16" class="append-right-5" />
- {{ outputText }}
+ {{ file.name }}
</span>
<component
:is="extraComponent"
@@ -185,17 +164,15 @@ export default {
/>
</div>
</div>
- <template v-if="file.opened">
+ <template v-if="file.opened || file.isHeader">
<file-row
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
- :level="level + 1"
+ :level="childFilesLevel"
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
- :display-text-key="displayTextKey"
- :should-truncate-start="shouldTruncateStart"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>
diff --git a/app/assets/javascripts/vue_shared/components/file_row_header.vue b/app/assets/javascripts/vue_shared/components/file_row_header.vue
new file mode 100644
index 00000000000..301fd8116a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/file_row_header.vue
@@ -0,0 +1,25 @@
+<script>
+import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
+
+const MAX_PATH_LENGTH = 40;
+
+export default {
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ truncatedPath() {
+ return truncatePathMiddleToLength(this.path, MAX_PATH_LENGTH);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="file-row-header bg-white sticky-top p-2 js-file-row-header">
+ <span class="bold">{{ truncatedPath }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
index 834c39a5ee0..4e5dfbf3bf8 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
@@ -1,15 +1,21 @@
<script>
import $ from 'jquery';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
/**
* Renders a split dropdown with
* an input that allows to search through the given
* array of options.
+ *
+ * When there are no results and `showCreateMode` is true
+ * it renders a create button with the value typed.
*/
export default {
name: 'FilteredSearchDropdown',
components: {
Icon,
+ GlButton,
},
props: {
title: {
@@ -43,6 +49,16 @@ export default {
type: String,
required: true,
},
+ showCreateMode: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ createButtonText: {
+ type: String,
+ required: false,
+ default: __('Create'),
+ },
},
data() {
return {
@@ -64,6 +80,12 @@ export default {
return this.items.slice(0, this.visibleItems);
},
+ computedCreateButtonText() {
+ return `${this.createButtonText} ${this.filter}`;
+ },
+ shouldRenderCreateButton() {
+ return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== '';
+ },
},
mounted() {
/**
@@ -112,10 +134,20 @@ export default {
<div class="dropdown-content">
<ul>
<li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result">
- <slot name="result" :result="result"> {{ result[filterKey] }} </slot>
+ <slot name="result" :result="result">{{ result[filterKey] }}</slot>
</li>
</ul>
</div>
+
+ <div v-if="shouldRenderCreateButton" class="dropdown-footer">
+ <slot name="footer" :filter="filter">
+ <gl-button
+ class="js-dropdown-create-button btn-transparent"
+ @click="$emit('createItem', filter)"
+ >{{ computedCreateButtonText }}</gl-button
+ >
+ </slot>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index faf4181bbaf..438851e5ac7 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -81,7 +81,7 @@ export default {
type="button"
class="close js-modal-close-action"
data-dismiss="modal"
- @click="emitCancel($event);"
+ @click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
@@ -96,7 +96,7 @@ export default {
type="button"
class="btn js-modal-cancel-action qa-modal-cancel-button"
data-dismiss="modal"
- @click="emitCancel($event);"
+ @click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
@@ -105,7 +105,7 @@ export default {
type="button"
class="btn js-modal-primary-action qa-modal-primary-button"
data-dismiss="modal"
- @click="emitSubmit($event);"
+ @click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
new file mode 100644
index 00000000000..df6fadf10cd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -0,0 +1,69 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlModal } from '@gitlab/ui';
+
+/**
+ * This component keeps the GlModal's visibility in sync with the given vuex module.
+ */
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ modalModule: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ isVisible(state) {
+ return state[this.modalModule].isVisible;
+ },
+ }),
+ attrs() {
+ const { modalId, modalModule, ...attrs } = this.$attrs;
+
+ return attrs;
+ },
+ },
+ watch: {
+ isVisible(val) {
+ return val ? this.bsShow() : this.bsHide();
+ },
+ },
+ methods: {
+ ...mapActions({
+ syncShow(dispatch) {
+ return dispatch(`${this.modalModule}/show`);
+ },
+ syncHide(dispatch) {
+ return dispatch(`${this.modalModule}/hide`);
+ },
+ }),
+ bsShow() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ bsHide() {
+ // $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ v-bind="attrs"
+ :modal-id="modalId"
+ v-on="$listeners"
+ @shown="syncShow"
+ @hidden="syncHide"
+ >
+ <slot></slot>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index c830f5b49b6..3f45dc7853b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -148,7 +148,7 @@ export default {
:class="action.cssClass"
container-class="d-inline"
:label="action.label"
- @click="onClickAction(action);"
+ @click="onClickAction(action)"
/>
</template>
</section>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
new file mode 100644
index 00000000000..27cfa8abb24
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
+
+export default {
+ name: 'IssueItem',
+ components: {
+ IssueMilestone,
+ IssueAssignees,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [relatedIssuableMixin],
+ props: {
+ canReorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ stateTitle() {
+ return sprintf(
+ '<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
+ {
+ state: this.isOpen ? __('Opened') : __('Closed'),
+ timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords,
+ timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp,
+ },
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'issuable-info-container': !canReorder,
+ 'card-body': canReorder,
+ }"
+ class="item-body"
+ >
+ <div class="item-contents">
+ <div class="item-title d-flex align-items-center">
+ <icon
+ v-if="hasState"
+ v-tooltip
+ :css-classes="iconClass"
+ :name="iconName"
+ :size="16"
+ :title="stateTitle"
+ :aria-label="state"
+ data-html="true"
+ />
+ <icon
+ v-if="confidential"
+ v-gl-tooltip
+ name="eye-slash"
+ :size="16"
+ :title="__('Confidential')"
+ class="confidential-icon append-right-4"
+ :aria-label="__('Confidential')"
+ />
+ <a :href="computedPath" class="sortable-link">{{ title }}</a>
+ </div>
+ <div class="item-meta">
+ <div class="d-flex align-items-center item-path-id">
+ <icon
+ v-if="hasState"
+ v-tooltip
+ :css-classes="iconClass"
+ :name="iconName"
+ :size="16"
+ :title="stateTitle"
+ :aria-label="state"
+ data-html="true"
+ />
+ <span v-tooltip :title="itemPath" class="path-id-text">{{ itemPath }}</span>
+ {{ pathIdSeparator }}{{ itemId }}
+ </div>
+ <div class="item-meta-child d-flex align-items-center">
+ <issue-milestone
+ v-if="hasMilestone"
+ :milestone="milestone"
+ class="d-flex align-items-center item-milestone"
+ />
+ <slot name="dueDate"></slot>
+ <slot name="weight"></slot>
+ </div>
+ <issue-assignees
+ v-if="assignees.length"
+ :assignees="assignees"
+ class="item-assignees d-inline-flex"
+ />
+ </div>
+ </div>
+ <button
+ v-if="canRemove"
+ ref="removeButton"
+ v-tooltip
+ :disabled="removeDisabled"
+ type="button"
+ class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button"
+ title="Remove"
+ aria-label="Remove"
+ @click="onRemoveRequest"
+ >
+ <icon :size="16" class="btn-item-remove-icon" name="close" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 2f7ed4a982c..3f607aa2a0a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -27,11 +27,6 @@ export default {
type: String,
required: true,
},
- markdownVersion: {
- type: Number,
- required: false,
- default: 0,
- },
addSpacingClasses: {
type: Boolean,
required: false,
@@ -89,7 +84,6 @@ export default {
return this.referencedUsers.length >= referencedUsersThreshold;
},
lineContent() {
- const FIRST_CHAR_REGEX = /^(\+|-)/;
const [firstSuggestion] = this.suggestions;
if (firstSuggestion) {
return firstSuggestion.from_content;
@@ -99,7 +93,7 @@ export default {
const { rich_text: richText, text } = this.line;
if (text) {
- return text.replace(FIRST_CHAR_REGEX, '');
+ return text;
}
return _.unescape(stripHtml(richText).replace(/\n/g, ''));
@@ -159,7 +153,7 @@ export default {
this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…');
this.$http
- .post(this.versionedPreviewPath(), { text })
+ .post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
.then(data => this.renderMarkdown(data))
.catch(() => new Flash(__('Error loading markdown preview')));
@@ -183,16 +177,9 @@ export default {
this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
}
- this.$nextTick(() => {
- $(this.$refs['markdown-preview']).renderGFM();
- });
- },
-
- versionedPreviewPath() {
- const { markdownPreviewPath, markdownVersion } = this;
- return `${markdownPreviewPath}${
- markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
- }markdown_version=${markdownVersion}`;
+ this.$nextTick()
+ .then(() => $(this.$refs['markdown-preview']).renderGFM())
+ .catch(() => new Flash(__('Error rendering markdown preview')));
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index bf4d42670ee..cc6ecdb0395 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -78,13 +78,8 @@ export default {
<div class="md-header">
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }" class="md-header-tab">
- <button
- class="js-write-link"
- tabindex="-1"
- type="button"
- @click="writeMarkdownTab($event);"
- >
- Write
+ <button class="js-write-link" tabindex="-1" type="button" @click="writeMarkdownTab($event)">
+ {{ __('Write') }}
</button>
</li>
<li :class="{ active: previewMarkdown }" class="md-header-tab">
@@ -92,38 +87,43 @@ export default {
class="js-preview-link js-md-preview-button"
tabindex="-1"
type="button"
- @click="previewMarkdownTab($event);"
+ @click="previewMarkdownTab($event)"
>
- Preview
+ {{ __('Preview') }}
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
- <toolbar-button tag="**" button-title="Add bold text" icon="bold" />
- <toolbar-button tag="*" button-title="Add italic text" icon="italic" />
- <toolbar-button :prepend="true" tag="> " button-title="Insert a quote" icon="quote" />
- <toolbar-button tag="`" tag-block="```" button-title="Insert code" icon="code" />
+ <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" />
+ <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
+ <toolbar-button
+ :prepend="true"
+ tag="> "
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ />
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
- button-title="Add a link"
+ :button-title="__('Add a link')"
icon="link"
/>
<toolbar-button
:prepend="true"
tag="* "
- button-title="Add a bullet list"
+ :button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
:prepend="true"
tag="1. "
- button-title="Add a numbered list"
+ :button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
:prepend="true"
tag="* [ ] "
- button-title="Add a task list"
+ :button-title="__('Add a task list')"
icon="task-done"
/>
<toolbar-button
@@ -144,11 +144,11 @@ export default {
/>
<button
v-gl-tooltip
- aria-label="Go full screen"
+ :aria-label="__('Go full screen')"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
tabindex="-1"
- title="Go full screen"
+ :title="__('Go full screen')"
type="button"
>
<icon name="screen-full" />
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index f98560f7336..a351ca62c94 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -42,7 +42,7 @@ export default {
</script>
<template>
- <div>
+ <div class="md-suggestion">
<suggestion-diff-header
class="qa-suggestion-diff-header"
:can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled"
@@ -50,7 +50,7 @@ export default {
:help-page-path="helpPagePath"
@apply="applySuggestion"
/>
- <table class="mb-3 md-suggestion-diff">
+ <table class="mb-3 md-suggestion-diff js-syntax-highlight code">
<tbody>
<!-- Old Line -->
<tr class="line_holder old">
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 563e2f94fcc..c5a2aa1f2af 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -42,7 +42,7 @@ export default {
<div class="md-suggestion-header border-bottom-0 mt-2">
<div class="qa-suggestion-diff-header font-weight-bold">
{{ __('Suggested change') }}
- <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')">
+ <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
<icon name="question-o" css-classes="link-highlight" />
</a>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 7c6dbee3e19..dcda701f049 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -82,13 +82,12 @@ export default {
// extracts the suggested lines from the markdown
// calculates a line number for each line
- const FIRST_CHAR_REGEX = /^(\+|-)/;
const newLines = suggestionEl.querySelectorAll('.line');
const fromLine = this.suggestions.length ? this.suggestions[0].from_line : this.fromLine;
const lines = [];
newLines.forEach((line, i) => {
- const content = `${line.innerText.replace(FIRST_CHAR_REGEX, '')}\n`;
+ const content = `${line.innerText}\n`;
const lineNumber = fromLine + i;
lines.push({ content, lineNumber });
});
@@ -130,7 +129,7 @@ export default {
<template>
<div>
- <div class="flash-container mt-3"></div>
+ <div class="flash-container js-suggestions-flash"></div>
<div v-show="isRendered" ref="container" v-html="noteHtml"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 09a64502819..f8983a3d29a 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -58,7 +58,7 @@ export default {
active: tab.isActive,
}"
>
- <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab);">
+ <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)">
{{ tab.name }}
<span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 31df26f7b05..b0af8399955 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -97,7 +97,7 @@ export default {
v-html="note.note_html"
></div>
<div v-if="hasMoreCommits" class="flex-list">
- <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
<icon :name="toggleIcon" :size="8" class="append-right-5" />
<span>Toggle commit list</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
index bf736a378dd..8d81940eb91 100644
--- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue
+++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
@@ -28,11 +28,12 @@ export default {
data() {
return {
size: this.startSize,
+ isDragging: false,
};
},
computed: {
className() {
- return `drag-${this.side}`;
+ return [`position-${this.side}-0`, { 'is-dragging': this.isDragging }];
},
cursorStyle() {
if (this.enabled) {
@@ -57,6 +58,7 @@ export default {
startDrag(e) {
if (this.enabled) {
e.preventDefault();
+ this.isDragging = true;
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
@@ -80,6 +82,7 @@ export default {
},
endDrag(e) {
e.preventDefault();
+ this.isDragging = false;
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
@@ -91,7 +94,7 @@ export default {
<div
:class="className"
:style="cursorStyle"
- class="drag-handle"
+ class="position-absolute position-top-0 position-bottom-0 drag-handle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 8bdb5bf22c2..fa502b9beb9 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,6 +1,7 @@
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
name: 'DatePicker',
@@ -8,7 +9,7 @@ export default {
label: {
type: String,
required: false,
- default: 'Date picker',
+ default: __('Date picker'),
},
selectedDate: {
type: Date,
@@ -40,6 +41,7 @@ export default {
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
+ firstDay: gon.first_day_of_week,
});
this.$el.append(this.calendar.el);
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue b/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
index b399c232937..881b5059d2a 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
@@ -26,7 +26,7 @@ export default {
</script>
<template>
- <span :class="sizeClass" class="avatar-container project-avatar">
+ <span :class="sizeClass" class="avatar-container rect-avatar project-avatar">
<project-avatar-image
v-if="project.avatar_url"
:link-href="project.path"
@@ -34,6 +34,12 @@ export default {
:img-alt="project.name"
:img-size="size"
/>
- <identicon v-else :entity-id="project.id" :entity-name="project.name" :size-class="sizeClass" />
+ <identicon
+ v-else
+ :entity-id="project.id"
+ :entity-name="project.name"
+ :size-class="sizeClass"
+ class="rect-avatar"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index 1c6c3fc4734..df19906309c 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -19,7 +19,7 @@ export default {
data() {
return {
script: {},
- scriptSrc: 'https://www.google.com/recaptcha/api.js',
+ scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js',
};
},
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
new file mode 100644
index 00000000000..3074ea859cc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -0,0 +1,35 @@
+<script>
+import $ from 'jquery';
+import 'select2/select2';
+
+export default {
+ name: 'Select2Select',
+ props: {
+ options: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mounted() {
+ $(this.$refs.dropdownInput)
+ .val(this.value)
+ .select2(this.options)
+ .on('change', event => this.$emit('input', event.target.value));
+ },
+
+ beforeDestroy() {
+ $(this.$refs.dropdownInput).select2('destroy');
+ },
+};
+</script>
+
+<template>
+ <input ref="dropdownInput" type="hidden" />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 82067129c57..45f01a6fced 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -4,6 +4,7 @@ import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
name: 'SidebarDatePicker',
@@ -42,7 +43,7 @@ export default {
label: {
type: String,
required: false,
- default: 'Date picker',
+ default: __('Date picker'),
},
selectedDate: {
type: Date,
@@ -134,7 +135,7 @@ export default {
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
- @click="newDateSelected(null);"
+ @click="newDateSelected(null)"
>
remove
</button>
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 01e655d27e5..8e0b08032f7 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -54,15 +54,14 @@ export default {
return this.pageInfo.nextPage;
},
getItems() {
- const total = this.pageInfo.totalPages;
- const { page } = this.pageInfo;
+ const { totalPages, nextPage, previousPage, page } = this.pageInfo;
const items = [];
if (page > 1) {
items.push({ title: FIRST, first: true });
}
- if (page > 1) {
+ if (previousPage) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
@@ -70,32 +69,34 @@ export default {
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);
+ if (totalPages) {
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, totalPages);
- for (let i = start; i <= end; i += 1) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
+ 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 (totalPages - 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) {
+ if (nextPage) {
items.push({ title: NEXT, next: true });
+ } else {
+ items.push({ title: NEXT, disabled: true, next: true });
}
- if (total - page >= 1) {
+ if (totalPages && totalPages - page >= 1) {
items.push({ title: LAST, last: true });
}
return items;
},
showPagination() {
- return this.pageInfo.totalPages > 1;
+ return this.pageInfo.nextPage || this.pageInfo.previousPage;
},
},
methods: {
@@ -149,9 +150,9 @@ export default {
}"
class="page-item"
>
- <a class="page-link" @click.prevent="changePage(item.title, item.disabled);">
+ <button type="button" class="page-link" @click="changePage(item.title, item.disabled)">
{{ item.title }}
- </a>
+ </button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index e833a8e0483..a6c1737dcab 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -67,7 +67,9 @@ export default {
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`;
+ // Only adds the width to the URL if its not a base64 data image
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.size}`;
return baseSrc;
},
resultantSrcAttribute() {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
new file mode 100644
index 00000000000..8eaf8386b99
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+import UserAvatarLink from './user_avatar_link.vue';
+
+export default {
+ components: {
+ UserAvatarLink,
+ GlButton,
+ },
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ breakpoint: {
+ type: Number,
+ required: false,
+ default: 10,
+ },
+ imgSize: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ emptyText: {
+ type: String,
+ required: false,
+ default: __('None'),
+ },
+ },
+ data() {
+ return {
+ isExpanded: false,
+ };
+ },
+ computed: {
+ visibleItems() {
+ if (!this.hasHiddenItems) {
+ return this.items;
+ }
+
+ return this.items.slice(0, this.breakpoint);
+ },
+ hasHiddenItems() {
+ return this.hasBreakpoint && !this.isExpanded && this.items.length > this.breakpoint;
+ },
+ hasBreakpoint() {
+ return this.breakpoint > 0 && this.items.length > this.breakpoint;
+ },
+ expandText() {
+ if (!this.hasHiddenItems) {
+ return '';
+ }
+
+ const count = this.items.length - this.breakpoint;
+
+ return sprintf(__('%{count} more'), { count });
+ },
+ },
+ methods: {
+ expand() {
+ this.isExpanded = true;
+ },
+ collapse() {
+ this.isExpanded = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!items.length">{{ emptyText }}</div>
+ <div v-else>
+ <user-avatar-link
+ v-for="item in visibleItems"
+ :key="item.id"
+ :link-href="item.web_url"
+ :img-src="item.avatar_url"
+ :img-alt="item.name"
+ :tooltip-text="item.name"
+ :img-size="imgSize"
+ />
+ <template v-if="hasBreakpoint">
+ <gl-button v-if="hasHiddenItems" variant="link" @click="expand"> {{ expandText }} </gl-button>
+ <gl-button v-else variant="link" @click="collapse"> {{ __('show less') }} </gl-button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index d24fe1b547e..f9773622001 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -28,10 +28,10 @@ export default {
},
computed: {
statusHtml() {
- if (this.user.status.emoji && this.user.status.message) {
- return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
- } else if (this.user.status.message) {
- return this.user.status.message;
+ if (this.user.status.emoji && this.user.status.message_html) {
+ return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
+ } else if (this.user.status.message_html) {
+ return this.user.status.message_html;
}
return '';
},
diff --git a/app/assets/javascripts/vue_shared/mixins/is_ee.js b/app/assets/javascripts/vue_shared/mixins/is_ee.js
new file mode 100644
index 00000000000..8e00d93ef18
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/is_ee.js
@@ -0,0 +1,10 @@
+import Vue from 'vue';
+import { isEE } from '~/lib/utils/common_utils';
+
+Vue.mixin({
+ computed: {
+ isEE() {
+ return isEE();
+ },
+ },
+});
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
new file mode 100644
index 00000000000..455ae832234
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -0,0 +1,155 @@
+import _ from 'underscore';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import tooltip from '~/vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+const mixins = {
+ data() {
+ return {
+ removeDisabled: false,
+ };
+ },
+ props: {
+ idKey: {
+ type: Number,
+ required: true,
+ },
+ displayReference: {
+ type: String,
+ required: true,
+ },
+ pathIdSeparator: {
+ type: String,
+ required: true,
+ },
+ eventNamespace: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ path: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ state: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ createdAt: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ closedAt: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ milestone: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ dueDate: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ weight: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ canRemove: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ computed: {
+ hasState() {
+ return this.state && this.state.length > 0;
+ },
+ isOpen() {
+ return this.state === 'opened';
+ },
+ isClosed() {
+ return this.state === 'closed';
+ },
+ hasTitle() {
+ return this.title.length > 0;
+ },
+ hasMilestone() {
+ return !_.isEmpty(this.milestone);
+ },
+ iconName() {
+ return this.isOpen ? 'issue-open-m' : 'issue-close';
+ },
+ iconClass() {
+ return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
+ },
+ computedLinkElementType() {
+ return this.path.length > 0 ? 'a' : 'span';
+ },
+ computedPath() {
+ return this.path.length ? this.path : null;
+ },
+ itemPath() {
+ return this.displayReference.split(this.pathIdSeparator)[0];
+ },
+ itemId() {
+ return this.displayReference.split(this.pathIdSeparator).pop();
+ },
+ createdAtInWords() {
+ return this.createdAt ? this.timeFormated(this.createdAt) : '';
+ },
+ createdAtTimestamp() {
+ return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
+ },
+ closedAtInWords() {
+ return this.closedAt ? this.timeFormated(this.closedAt) : '';
+ },
+ closedAtTimestamp() {
+ return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
+ },
+ },
+ methods: {
+ onRemoveRequest() {
+ let namespacePrefix = '';
+ if (this.eventNamespace && this.eventNamespace.length > 0) {
+ namespacePrefix = `${this.eventNamespace}`;
+ }
+
+ this.$emit(`${namespacePrefix}RemoveRequest`, this.idKey);
+
+ this.removeDisabled = true;
+ },
+ },
+};
+
+export default mixins;
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
new file mode 100644
index 00000000000..7b209909f69
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
@@ -0,0 +1,20 @@
+import * as types from './mutation_types';
+
+export const open = ({ commit }, data) => {
+ commit(types.OPEN, data);
+};
+
+export const close = ({ commit }) => {
+ commit(types.CLOSE);
+};
+
+export const show = ({ commit }) => {
+ commit(types.SHOW);
+};
+
+export const hide = ({ commit }) => {
+ commit(types.HIDE);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/index.js b/app/assets/javascripts/vuex_shared/modules/modal/index.js
new file mode 100644
index 00000000000..c349d875c24
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/index.js
@@ -0,0 +1,10 @@
+import state from './state';
+import mutations from './mutations';
+import * as actions from './actions';
+
+export default () => ({
+ namespaced: true,
+ state: state(),
+ mutations,
+ actions,
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js
new file mode 100644
index 00000000000..f8259736009
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js
@@ -0,0 +1,4 @@
+export const HIDE = 'HIDE';
+export const SHOW = 'SHOW';
+export const OPEN = 'OPEN';
+export const CLOSE = 'CLOSE';
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutations.js b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js
new file mode 100644
index 00000000000..9e96ae8b5a9
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js
@@ -0,0 +1,18 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SHOW](state) {
+ state.isVisible = true;
+ },
+ [types.HIDE](state) {
+ state.isVisible = false;
+ },
+ [types.OPEN](state, data) {
+ state.data = data;
+ state.isVisible = true;
+ },
+ [types.CLOSE](state) {
+ state.data = null;
+ state.isVisible = false;
+ },
+};
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/state.js b/app/assets/javascripts/vuex_shared/modules/modal/state.js
new file mode 100644
index 00000000000..5d0955aa9b0
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/state.js
@@ -0,0 +1,4 @@
+export default () => ({
+ isVisible: false,
+ data: null,
+});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index bdf20866197..83ad8766cb5 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -40,16 +40,6 @@
@import "components/**/*";
/*
- * Code highlight
- */
-@import "highlight/dark";
-@import "highlight/monokai";
-@import "highlight/solarized_dark";
-@import "highlight/solarized_light";
-@import "highlight/white";
-@import "highlight/none";
-
-/*
* Styles for JS behaviors.
*/
@import "behaviors";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index f0671e36130..c8357f7751c 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -70,6 +70,17 @@ h6,
margin-bottom: 10px;
}
+/* Our adjustments to hx & .hx above add unnecessary margins to modal-title
+ and page-title in modals, so we set them to 0 in order to have properly
+ formatted modal headers. */
+.modal-header {
+ .modal-title,
+ .page-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
h5,
.h5 {
font-size: $gl-font-size;
@@ -134,7 +145,8 @@ table {
pointer-events: none;
}
-.popover {
+.popover,
+.popover-header {
font-size: 14px;
}
@@ -142,7 +154,9 @@ table {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
- .d#{$infix}-table-header-group { display: table-header-group !important; }
+ .d#{$infix}-table-header-group {
+ display: table-header-group !important;
+ }
}
}
@@ -211,7 +225,7 @@ h3.popover-header {
}
.info-well {
- background: $theme-gray-50;
+ background: $gray-50;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 4px;
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
new file mode 100644
index 00000000000..edf7b26ebaa
--- /dev/null
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -0,0 +1,380 @@
+$item-path-max-width: 160px;
+$item-milestone-max-width: 120px;
+$item-weight-max-width: 48px;
+
+.related-items-list {
+ padding: $gl-padding-4;
+
+ &,
+ .list-item:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.item-body {
+ display: flex;
+ position: relative;
+ align-items: center;
+ padding: $gl-padding-8;
+ line-height: $gl-line-height;
+
+ .item-contents {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ flex-grow: 1;
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed,
+ .confidential-icon,
+ .item-milestone .icon,
+ .item-weight .board-card-info-icon {
+ min-width: $gl-padding;
+ cursor: help;
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ margin-right: $gl-padding-4;
+ }
+
+ .confidential-icon {
+ align-self: baseline;
+ color: $orange-600;
+ margin-right: $gl-padding-4;
+ }
+
+ .item-title {
+ flex-basis: 100%;
+ margin-bottom: $gl-padding-8;
+ font-size: $gl-font-size-small;
+
+ &.mr-title {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .sortable-link {
+ max-width: 85%;
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: none;
+ }
+ }
+
+ .item-meta {
+ display: flex;
+ flex-wrap: wrap;
+ flex-basis: 100%;
+ font-size: $gl-font-size-small;
+ color: $gl-text-color-secondary;
+
+ .item-meta-child {
+ order: 0;
+ display: flex;
+ flex-wrap: wrap;
+ flex-basis: 100%;
+
+ .item-due-date,
+ .item-weight {
+ margin-left: $gl-padding-8;
+ }
+
+ .item-milestone,
+ .item-weight {
+ cursor: help;
+ }
+
+ .item-milestone {
+ text-decoration: none;
+ max-width: $item-milestone-max-width;
+ }
+
+ .item-due-date {
+ margin-right: 0;
+ }
+
+ .item-weight {
+ margin-right: 0;
+ max-width: $item-weight-max-width;
+ }
+ }
+
+ .item-path-id .path-id-text,
+ .item-milestone .milestone-title,
+ .item-due-date,
+ .item-weight .board-card-info-text {
+ color: $gl-text-color-secondary;
+ display: inline-block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .item-path-id {
+ margin-top: $gl-padding-4;
+ font-size: $gl-font-size-xs;
+ white-space: nowrap;
+
+ .path-id-text {
+ font-weight: $gl-font-weight-bold;
+ max-width: $item-path-max-width;
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: block;
+ }
+
+ &:not(.mr-item-path) {
+ order: 1;
+ }
+ }
+
+ .item-milestone .ic-clock {
+ color: $gl-text-color-tertiary;
+ margin-right: $gl-padding-4;
+ }
+
+ .item-assignees {
+ order: 2;
+ align-self: flex-end;
+ align-items: center;
+ margin-left: auto;
+
+ .user-avatar-link {
+ margin-right: -$gl-padding-4;
+
+ &:nth-of-type(1) {
+ z-index: 2;
+ }
+
+ &:nth-of-type(2) {
+ z-index: 1;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .avatar {
+ height: $gl-padding;
+ width: $gl-padding;
+ margin-right: 0;
+ vertical-align: bottom;
+ }
+
+ .avatar-counter {
+ height: $gl-padding;
+ border: 1px solid transparent;
+ background-color: $gl-text-color-tertiary;
+ font-weight: $gl-font-weight-bold;
+ padding: 0 $gl-padding-4;
+ line-height: $gl-padding;
+ }
+ }
+ }
+
+ .btn-item-remove {
+ position: absolute;
+ right: 0;
+ top: $gl-padding-4 / 2;
+ padding: $gl-padding-4;
+ margin-right: $gl-padding-4 / 2;
+ line-height: 0;
+ border-color: transparent;
+ color: $gl-text-color-secondary;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+}
+
+.mr-status-wrapper,
+.mr-ci-status {
+ line-height: 0;
+}
+
+@include media-breakpoint-up(sm) {
+ .item-body {
+ .item-contents .item-title {
+ .mr-title-link,
+ .sortable-link {
+ max-width: 90%;
+ }
+ }
+ }
+}
+
+/* Small devices (landscape phones, 768px and up) */
+@include media-breakpoint-up(md) {
+ .item-body {
+ .item-contents {
+ min-width: 0;
+
+ .item-title {
+ flex-basis: unset;
+ // 95% because we compensate
+ // for remove button which is
+ // positioned absolutely
+ width: 95%;
+ margin-bottom: $gl-padding-4;
+
+ .mr-title-link,
+ .sortable-link {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 100%;
+ }
+ }
+
+ .item-meta {
+ .item-path-id {
+ order: 0;
+ margin-top: 0;
+ }
+
+ .item-meta-child {
+ flex-basis: unset;
+ margin-left: auto;
+ margin-right: $gl-padding-4;
+
+ ~ .item-assignees {
+ margin-left: $gl-padding-4;
+ }
+ }
+
+ .item-assignees {
+ margin-bottom: 0;
+ margin-left: 0;
+ order: 2;
+ }
+ }
+ }
+
+ .btn-item-remove {
+ order: 1;
+ }
+ }
+}
+
+/* Medium devices (desktops, 992px and up) */
+@include media-breakpoint-up(lg) {
+ .item-body {
+ padding: $gl-padding;
+
+ .item-title {
+ font-size: $gl-font-size;
+ }
+
+ .item-meta .item-path-id {
+ font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small`
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ margin-right: $gl-padding-4;
+ }
+ }
+}
+
+/* Large devices (large desktops, 1200px and up) */
+@include media-breakpoint-up(xl) {
+ .item-body {
+ padding: $gl-padding-8;
+ padding-left: $gl-padding;
+
+ .item-contents {
+ flex-wrap: nowrap;
+ overflow: hidden;
+
+ .item-title {
+ display: flex;
+ margin-bottom: 0;
+ min-width: 0;
+ width: auto;
+ flex-basis: unset;
+ font-weight: $gl-font-weight-normal;
+
+ .mr-title-link,
+ .sortable-link {
+ display: block;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: block;
+ margin-right: $gl-padding-8;
+ }
+
+ .confidential-icon {
+ align-self: auto;
+ margin-top: 0;
+ }
+ }
+
+ .item-meta {
+ margin-top: 0;
+ justify-content: flex-end;
+ flex: 1;
+ flex-wrap: nowrap;
+
+ .item-path-id {
+ order: 0;
+ margin-top: 0;
+ margin-left: $gl-padding-8;
+ margin-right: auto;
+
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: none;
+ }
+ }
+
+ .item-meta-child {
+ margin-left: $gl-padding-8;
+ flex-wrap: nowrap;
+ }
+
+ .item-assignees {
+ flex-grow: 0;
+ margin-top: 0;
+ margin-right: $gl-padding-4;
+
+ .avatar {
+ height: $gl-padding-24;
+ width: $gl-padding-24;
+ }
+
+ .avatar-counter {
+ height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ line-height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ }
+ }
+ }
+ }
+
+ .btn-item-remove {
+ position: relative;
+ align-self: center;
+ top: initial;
+ right: 0;
+ margin-right: 0;
+ padding: $btn-sm-side-margin;
+
+ &:hover {
+ border-color: $border-color;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 834e7ffce81..216877a4461 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -31,7 +31,6 @@
@import 'framework/logo';
@import 'framework/markdown_area';
@import 'framework/media_object';
-@import 'framework/mobile';
@import 'framework/modal';
@import 'framework/pagination';
@import 'framework/panels';
@@ -66,3 +65,5 @@
@import 'framework/terms';
@import 'framework/read_more';
@import 'framework/flex_grid';
+@import 'framework/system_messages';
+@import "framework/spinner";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 549a8730301..257d788873c 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -27,7 +27,7 @@
&.flipOutY,
&.bounceIn,
&.bounceOut {
- @include webkit-prefix(animation-duration, .75s);
+ @include webkit-prefix(animation-duration, 0.75s);
}
&.short {
@@ -63,56 +63,44 @@
//
// Pass in any number of transitions
@mixin transition($transitions...) {
- $unfoldedTransitions: ();
+ $unfolded-transitions: ();
@each $transition in $transitions {
- $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
+ $unfolded-transitions: append($unfolded-transitions, unfold-transition($transition), comma);
}
- transition: $unfoldedTransitions;
+ transition: $unfolded-transitions;
}
-@mixin disableAllAnimation {
+@mixin disable-all-animation {
/*CSS transitions*/
- -o-transition-property: none !important;
- -moz-transition-property: none !important;
- -ms-transition-property: none !important;
- -webkit-transition-property: none !important;
transition-property: none !important;
/*CSS transforms*/
- -o-transform: none !important;
- -moz-transform: none !important;
- -ms-transform: none !important;
- -webkit-transform: none !important;
transform: none !important;
/*CSS animations*/
- -webkit-animation: none !important;
- -moz-animation: none !important;
- -o-animation: none !important;
- -ms-animation: none !important;
animation: none !important;
}
-@function unfoldTransition ($transition) {
+@function unfold-transition ($transition) {
// Default values
$property: all;
$duration: $general-hover-transition-duration;
$easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
$delay: null; // Browser default is 0, which is what we want
- $defaultProperties: ($property, $duration, $easing, $delay);
+ $default-properties: ($property, $duration, $easing, $delay);
// Grab transition properties if they exist
- $unfoldedTransition: ();
- @for $i from 1 through length($defaultProperties) {
+ $unfolded-transition: ();
+ @for $i from 1 through length($default-properties) {
$p: null;
@if $i <= length($transition) {
$p: nth($transition, $i);
} @else {
- $p: nth($defaultProperties, $i);
+ $p: nth($default-properties, $i);
}
- $unfoldedTransition: append($unfoldedTransition, $p);
+ $unfolded-transition: append($unfolded-transition, $p);
}
- @return $unfoldedTransition;
+ @return $unfolded-transition;
}
.btn {
@@ -202,9 +190,9 @@ a {
}
}
- [class^="skeleton-line-"] {
+ [class^='skeleton-line-'] {
position: relative;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
height: 10px;
overflow: hidden;
@@ -218,13 +206,11 @@ a {
animation: blockTextShine 1s linear infinite forwards;
background-repeat: no-repeat;
background-size: cover;
- background-image: linear-gradient(
- to right,
- $theme-gray-100 0%,
- $theme-gray-50 20%,
- $theme-gray-100 40%,
- $theme-gray-100 100%
- );
+ background-image: linear-gradient(to right,
+ $gray-100 0%,
+ $gray-50 20%,
+ $gray-100 40%,
+ $gray-100 100%);
height: 10px;
}
}
@@ -260,3 +246,25 @@ $skeleton-line-widths: (
.slide-down-leave-to {
transform: translateY(-30%);
}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg);}
+ 100% { transform: rotate(360deg);}
+}
+
+/** COMMON ANIMATION CLASSES **/
+.transform-origin-center { @include webkit-prefix(transform-origin, 50% 50%); }
+.animate-n-spin { @include webkit-prefix(animation-name, spin); }
+.animate-c-infinite { @include webkit-prefix(animation-iteration-count, infinite); }
+.animate-t-linear { @include webkit-prefix(animation-timing-function, linear); }
+.animate-d-1 { @include webkit-prefix(animation-duration, 1s); }
+.animate-d-2 { @include webkit-prefix(animation-duration, 2s); }
+
+/** COMPOSITE ANIMATION CLASSES **/
+.gl-spinner {
+ @include webkit-prefix(animation-name, spin);
+ @include webkit-prefix(animation-iteration-count, infinite);
+ @include webkit-prefix(animation-timing-function, linear);
+ @include webkit-prefix(animation-duration, 1s);
+ transform-origin: 50% 50%;
+}
diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss
index 62493c32833..1586265d40e 100644
--- a/app/assets/stylesheets/framework/asciidoctor.scss
+++ b/app/assets/stylesheets/framework/asciidoctor.scss
@@ -1,7 +1,7 @@
.admonitionblock td.icon {
width: 1%;
- [class^="fa icon-"] {
+ [class^='fa icon-'] {
@extend .fa-2x;
}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index e132aa4c216..af79a4d9392 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -45,7 +45,6 @@
display: inline-block;
margin-left: 2px;
flex-shrink: 0;
- -webkit-flex-shrink: 0;
&.s16 { margin-right: 4px; }
&.s24 { margin-right: 4px; }
@@ -74,20 +73,48 @@
background-color: $gray-darker;
// Sizes
- &.s16 { font-size: 12px; line-height: 1.33; }
- &.s24 { font-size: 13px; line-height: 1.8; }
- &.s26 { font-size: 20px; line-height: 1.33; }
- &.s32 { font-size: 20px; line-height: 30px; }
- &.s40 { font-size: 16px; line-height: 38px; }
- &.s48 { font-size: 20px; line-height: 46px; }
- &.s60 { font-size: 32px; line-height: 58px; }
- &.s64 { font-size: 32px; line-height: 64px; }
- &.s70 { font-size: 34px; line-height: 70px; }
- &.s90 { font-size: 36px; line-height: 88px; }
- &.s100 { font-size: 36px; line-height: 98px; }
- &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
- &.s140 { font-size: 72px; line-height: 138px; }
- &.s160 { font-size: 96px; line-height: 158px; }
+ &.s16 { font-size: 12px;
+ line-height: 1.33; }
+
+ &.s24 { font-size: 13px;
+ line-height: 1.8; }
+
+ &.s26 { font-size: 20px;
+ line-height: 1.33; }
+
+ &.s32 { font-size: 20px;
+ line-height: 30px; }
+
+ &.s40 { font-size: 16px;
+ line-height: 38px; }
+
+ &.s48 { font-size: 20px;
+ line-height: 46px; }
+
+ &.s60 { font-size: 32px;
+ line-height: 58px; }
+
+ &.s64 { font-size: 32px;
+ line-height: 64px; }
+
+ &.s70 { font-size: 34px;
+ line-height: 70px; }
+
+ &.s90 { font-size: 36px;
+ line-height: 88px; }
+
+ &.s100 { font-size: 36px;
+ line-height: 98px; }
+
+ &.s110 { font-size: 40px;
+ line-height: 108px;
+ font-weight: $gl-font-weight-normal; }
+
+ &.s140 { font-size: 72px;
+ line-height: 138px; }
+
+ &.s160 { font-size: 96px;
+ line-height: 158px; }
// Background colors
&.bg1 { background-color: $identicon-red; }
@@ -120,8 +147,35 @@
align-self: center;
}
- &.s40 { min-width: 40px; min-height: 40px; }
- &.s64 { min-width: 64px; min-height: 64px; }
+ &.s40 { min-width: 40px;
+ min-height: 40px; }
+
+ &.s64 { min-width: 64px;
+ min-height: 64px; }
+}
+
+.rect-avatar {
+ border-radius: $border-radius-small;
+ &.s16 { border-radius: $border-radius-small; }
+ &.s18 { border-radius: $border-radius-small; }
+ &.s19 { border-radius: $border-radius-small; }
+ &.s20 { border-radius: $border-radius-small; }
+ &.s24 { border-radius: $border-radius-default; }
+ &.s26 { border-radius: $border-radius-default; }
+ &.s32 { border-radius: $border-radius-default; }
+ &.s36 { border-radius: $border-radius-default; }
+ &.s40 { border-radius: $border-radius-default; }
+ &.s46 { border-radius: $border-radius-default; }
+ &.s48 { border-radius: $border-radius-large; }
+ &.s60 { border-radius: $border-radius-large; }
+ &.s64 { border-radius: $border-radius-large; }
+ &.s70 { border-radius: $border-radius-large; }
+ &.s90 { border-radius: $border-radius-large; }
+ &.s96 { border-radius: $border-radius-large; }
+ &.s100 { border-radius: $border-radius-large; }
+ &.s110 { border-radius: $border-radius-large; }
+ &.s140 { border-radius: $border-radius-large; }
+ &.s160 { border-radius: $border-radius-large; }
}
.avatar-counter {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 7a95db5976d..648e1944388 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -15,7 +15,7 @@
margin-top: 3px;
padding: $gl-padding;
z-index: 300;
- width: 300px;
+ width: $award-emoji-width;
font-size: 14px;
background-color: $white-light;
border: 1px solid $border-white-light;
@@ -23,9 +23,9 @@
box-shadow: 0 6px 12px $award-emoji-menu-shadow;
pointer-events: none;
opacity: 0;
- transform: scale(.2);
+ transform: scale(0.2);
transform-origin: 0 -45px;
- transition: .3s cubic-bezier(.67, .06, .19, 1.44);
+ transition: 0.3s cubic-bezier(0.67, 0.06, 0.19, 1.44);
transition-property: transform, opacity;
&.is-rendered {
@@ -55,10 +55,14 @@
transform: none;
}
}
+
+ @include media-breakpoint-down(xs) {
+ width: $award-emoji-width-xs;
+ }
}
.emoji-search {
- background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC');
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
@@ -86,7 +90,7 @@
background: none;
border: 0;
border-radius: $border-radius-base;
- transition: transform .15s cubic-bezier(.3, 0, .2, 2);
+ transition: transform 0.15s cubic-bezier(0.3, 0, 0.2, 2);
&:hover {
background-color: transparent;
@@ -176,7 +180,7 @@
&.user-authored {
cursor: default;
background-color: $gray-light;
- border-color: $theme-gray-200;
+ border-color: $gray-200;
color: $gl-text-color-disabled;
gl-emoji {
@@ -229,10 +233,10 @@
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
+ }
- path {
- fill: $border-gray-normal;
- }
+ path {
+ fill: $border-gray-normal;
}
}
@@ -243,6 +247,10 @@
left: 10px;
bottom: 6px;
opacity: 0;
+
+ path {
+ fill: $award-emoji-positive-add-lines;
+ }
}
.award-control-text {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 43b7c26b272..e6c55252b24 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -86,11 +86,8 @@
}
.block-controls {
- display: -webkit-flex;
display: flex;
- -webkit-justify-content: flex-end;
justify-content: flex-end;
- -webkit-flex: 1;
flex: 1;
.control {
@@ -153,7 +150,7 @@
display: inline-block;
margin-left: 5px;
font-size: 18px;
- color: color("gray");
+ color: color('gray');
}
p {
@@ -228,7 +225,6 @@
}
.group-info {
-
h1 {
display: inline;
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index a4a9276c580..a4af84f8d27 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -42,6 +42,10 @@
color: $text;
border-color: $border;
+ &.btn-border-color {
+ border-color: $border-color;
+ }
+
> .icon {
color: $text;
}
@@ -162,7 +166,8 @@
@include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
- &.btn-remove {
+ &.btn-remove,
+ &.btn-danger {
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
@@ -390,8 +395,6 @@
cursor: default;
&:active {
- -moz-box-shadow: inset 0 0 0 $white-light;
- -webkit-box-shadow: inset 0 0 0 $white-light;
box-shadow: inset 0 0 0 $white-light;
}
}
@@ -483,7 +486,7 @@
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
- border-color: $theme-gray-200 !important;
+ border-color: $gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e037b02a30c..aad5150c0b5 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -48,7 +48,20 @@
color: $brand-info;
}
-.hint { font-style: italic; color: $gl-gray-400; }
+.text-break-word {
+ word-break: break-all;
+}
+
+.text-underline,
+.text-underline:hover {
+ text-decoration: underline;
+}
+
+.hint {
+ font-style: italic;
+ color: $gl-gray-400;
+}
+
.light { color: $gl-text-color; }
.slead {
@@ -89,6 +102,10 @@ hr {
.str-truncated {
@include str-truncated;
+ &-30 {
+ @include str-truncated(30%);
+ }
+
&-60 {
@include str-truncated(60%);
}
@@ -145,13 +162,14 @@ p.time {
text-shadow: none;
}
-.thin_area {
+.thin-area {
height: 150px;
}
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
img { max-width: 100%; }
+
.note-title {
li {
border-bottom: 0 !important;
@@ -322,7 +340,7 @@ img.emoji {
.disabled-content {
pointer-events: none;
- opacity: .5;
+ opacity: 0.5;
}
.break-word {
@@ -381,13 +399,35 @@ img.emoji {
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
+.vertical-align-sub { vertical-align: sub; }
.flex-align-self-center { align-self: center; }
.flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; }
-.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
+.ws-normal { white-space: normal; }
+.overflow-auto { overflow: auto; }
+
+.d-flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/** COMMON SIZING CLASSES **/
+.w-0 { width: 0; }
+
+.h-12em { height: 12em; }
+
+.mw-460 { max-width: 460px; }
+.mw-6em { max-width: 6em; }
+
.min-height-0 { min-height: 0; }
+.w-3 { width: #{3 * $grid-size}; }
+
+.h-3 { width: #{3 * $grid-size}; }
+
+/** COMMON SPACING CLASSES **/
.gl-pl-0 { padding-left: 0; }
.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
.gl-pl-2 { padding-left: $grid-size; }
@@ -401,3 +441,36 @@ img.emoji {
.gl-pr-3 { padding-right: #{2 * $grid-size}; }
.gl-pr-4 { padding-right: #{3 * $grid-size}; }
.gl-pr-5 { padding-right: #{4 * $grid-size}; }
+
+/**
+ * Removes browser specific clear icon from input fields in
+ * Internet Explorer 10, Internet Explorer 11, and Microsoft Edge.
+ * This is intended for elements which add a customized clear icon.
+ *
+ * see also https://developer.mozilla.org/en-US/docs/Web/CSS/::-ms-clear
+ */
+.ms-no-clear ::-ms-clear {
+ display: none;
+}
+
+/** COMMON POSITIONING CLASSES */
+.position-bottom-0 { bottom: 0; }
+.position-left-0 { left: 0; }
+.position-right-0 { right: 0; }
+.position-top-0 { top: 0; }
+
+.drag-handle {
+ width: 4px;
+
+ &:hover {
+ background-color: $white-normal;
+ }
+
+ &.is-dragging {
+ background-color: $gray-600;
+ }
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 8b6a7017c47..3238b01c6c0 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -5,7 +5,7 @@
padding-left: $contextual-sidebar-collapsed-width;
}
- @include media-breakpoint-up(lg) {
+ @include media-breakpoint-up(xl) {
padding-left: $contextual-sidebar-width;
}
@@ -15,7 +15,7 @@
}
.page-with-icon-sidebar {
- @include media-breakpoint-up(sm) {
+ @include media-breakpoint-up(md) {
padding-left: $contextual-sidebar-collapsed-width;
}
}
@@ -71,6 +71,44 @@
}
}
+@mixin collapse-contextual-sidebar-content {
+ .context-header {
+ height: 60px;
+ width: $contextual-sidebar-collapsed-width;
+
+ a {
+ padding: 10px 4px;
+ }
+ }
+
+ .sidebar-top-level-items > li {
+ .sidebar-sub-level-items {
+ &:not(.flyout-list) {
+ display: none;
+ }
+ }
+ }
+
+ .nav-icon-container {
+ margin-right: 0;
+ }
+
+ .toggle-sidebar-button {
+ padding: 16px;
+ width: $contextual-sidebar-collapsed-width - 1px;
+
+ .collapse-text,
+ .icon-angle-double-left {
+ display: none;
+ }
+
+ .icon-angle-double-right {
+ display: block;
+ margin: 0;
+ }
+ }
+}
+
.nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
@@ -89,7 +127,7 @@
}
}
- &.sidebar-collapsed-desktop {
+ @mixin collapse-contextual-sidebar {
width: $contextual-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
@@ -115,6 +153,10 @@
}
}
+ &.sidebar-collapsed-desktop {
+ @include collapse-contextual-sidebar;
+ }
+
&.sidebar-expanded-mobile {
left: 0;
}
@@ -150,7 +192,7 @@
}
}
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(sm) {
left: (-$contextual-sidebar-width);
}
@@ -167,16 +209,19 @@
height: 16px;
width: 16px;
}
+
+ @media (min-width: map-get($grid-breakpoints, md)) and (max-width: map-get($grid-breakpoints, xl) - 1px) {
+ &:not(.sidebar-expanded-mobile) {
+ @include collapse-contextual-sidebar;
+ @include collapse-contextual-sidebar-content;
+ }
+ }
}
.nav-sidebar-inner-scroll {
height: 100%;
width: 100%;
overflow: auto;
-
- @include media-breakpoint-up(sm) {
- overflow: hidden;
- }
}
.with-performance-bar .nav-sidebar {
@@ -346,53 +391,13 @@
}
}
-.toggle-sidebar-button {
- @include media-breakpoint-down(xs) {
- display: none;
- }
-}
-
.collapse-text {
white-space: nowrap;
overflow: hidden;
}
.sidebar-collapsed-desktop {
- .context-header {
- height: 60px;
- width: $contextual-sidebar-collapsed-width;
-
- a {
- padding: 10px 4px;
- }
- }
-
- .sidebar-top-level-items > li {
- .sidebar-sub-level-items {
- &:not(.flyout-list) {
- display: none;
- }
- }
- }
-
- .nav-icon-container {
- margin-right: 0;
- }
-
- .toggle-sidebar-button {
- padding: 16px;
- width: $contextual-sidebar-collapsed-width - 1px;
-
- .collapse-text,
- .icon-angle-double-left {
- display: none;
- }
-
- .icon-angle-double-right {
- display: block;
- margin: 0;
- }
- }
+ @include collapse-contextual-sidebar-content;
}
.fly-out-top-item {
@@ -428,16 +433,14 @@
color: $gl-text-color-secondary;
}
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(sm) {
display: flex;
align-items: center;
i {
font-size: 18px;
}
- }
- @include media-breakpoint-down(xs) {
+ .breadcrumbs-links {
padding-left: $gl-padding;
border-left: 1px solid $gl-text-color-quaternary;
@@ -445,21 +448,25 @@
}
}
-@include media-breakpoint-down(xs) {
+@include media-breakpoint-down(sm) {
.close-nav-button {
display: flex;
}
-}
-.mobile-overlay {
- display: none;
+ .toggle-sidebar-button {
+ display: none;
+ }
- &.mobile-nav-open {
- display: block;
- position: fixed;
- background-color: $black-transparent;
- height: 100%;
- width: 100%;
- z-index: 300;
+ .mobile-overlay {
+ display: none;
+
+ &.mobile-nav-open {
+ display: block;
+ position: fixed;
+ background-color: $black-transparent;
+ height: 100%;
+ width: 100%;
+ z-index: 300;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index afcb230797a..b90db135b4a 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -32,6 +32,10 @@
max-height: $dropdown-max-height;
overflow-y: auto;
+ &.dropdown-extended-height {
+ max-height: 400px;
+ }
+
@include media-breakpoint-down(xs) {
width: 100%;
}
@@ -125,7 +129,7 @@
@extend .dropdown-toggle;
padding-right: 25px;
position: relative;
- width: 163px;
+ width: 160px;
text-overflow: ellipsis;
overflow: hidden;
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index be85e03430e..13c5541da92 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -2,7 +2,7 @@ gl-emoji {
font-style: normal;
display: inline-flex;
vertical-align: middle;
- font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1.4em;
line-height: 1em;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 037a5adfb7e..6108eaa1ad0 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -4,6 +4,7 @@
*/
.file-holder {
border: 1px solid $border-color;
+ border-top: 0;
border-radius: $border-radius-default;
&.file-holder-no-border {
@@ -51,6 +52,7 @@
position: absolute;
top: 5px;
right: 15px;
+ margin-left: auto;
.btn {
padding: 0 10px;
@@ -324,10 +326,12 @@ span.idiff {
&,
.file-holder & {
display: flex;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border-bottom: 1px solid $border-color;
+ border-top: 1px solid $border-color;
padding: 5px $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
@@ -365,16 +369,12 @@ span.idiff {
margin: 0 10px 0 0;
}
- .file-actions {
- white-space: nowrap;
-
- .btn {
- padding: 0 10px;
- font-size: 13px;
- line-height: 28px;
- display: inline-block;
- float: none;
- }
+ .file-actions .btn {
+ padding: 0 10px;
+ font-size: 13px;
+ line-height: 28px;
+ display: inline-block;
+ float: none;
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index f48b3ddc912..e5b529ae11d 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -50,19 +50,15 @@
}
.filtered-search-wrapper {
- display: -webkit-flex;
display: flex;
@include media-breakpoint-down(xs) {
- -webkit-flex-direction: column;
flex-direction: column;
}
.tokens-container {
- display: -webkit-flex;
display: flex;
flex: 1;
- -webkit-flex: 1;
padding-left: 12px;
position: relative;
margin-bottom: 0;
@@ -82,21 +78,18 @@
.input-token:only-child,
.input-token:last-child {
flex: 1;
- -webkit-flex: 1;
max-width: inherit;
}
}
.filtered-search-token,
.filtered-search-term {
- display: -webkit-flex;
display: flex;
flex-shrink: 0;
margin-top: 4px;
margin-bottom: 4px;
.selectable {
- display: -webkit-flex;
display: flex;
}
@@ -176,7 +169,6 @@
}
.scroll-container {
- display: -webkit-flex;
display: flex;
overflow-x: auto;
white-space: nowrap;
@@ -186,7 +178,6 @@
.filtered-search-box {
position: relative;
flex: 1;
- display: -webkit-flex;
display: flex;
width: 100%;
min-width: 0;
@@ -194,7 +185,6 @@
background-color: $white-light;
@include media-breakpoint-down(xs) {
- -webkit-flex: 1 1 auto;
flex: 1 1 auto;
margin-bottom: 10px;
}
@@ -349,7 +339,6 @@
}
.filter-dropdown-container {
- display: -webkit-flex;
display: flex;
.dropdown-toggle {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4da2243981e..3b1d1d67509 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -36,6 +36,15 @@ label {
}
}
+.label-wrapper {
+ display: block;
+ margin: 0;
+}
+
+.form-label {
+ @extend label;
+}
+
.form-control-label {
@extend .col-md-2;
}
@@ -147,8 +156,6 @@ label {
.select-control {
padding-left: 10px;
padding-right: 10px;
- -webkit-appearance: none;
- -moz-appearance: none;
appearance: none;
&::-ms-expand {
@@ -169,7 +176,8 @@ label {
font-weight: $gl-font-weight-normal;
}
-.form-control::-webkit-input-placeholder {
+
+.form-control::placeholder {
color: $gl-text-color-tertiary;
}
@@ -252,7 +260,7 @@ label {
right: 0.8em;
top: 50%;
transform: translateY(-50%);
- color: $theme-gray-600;
+ color: $gray-600;
}
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 50d4298d418..6943bfbc3d0 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -32,7 +32,7 @@
height: $chip-size;
background: $white-light;
background-image: linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%),
- linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%);
+ linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%);
background-size: $bg-size $bg-size;
background-position: 0 0, $bg-pos $bg-pos;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 0ef50e139f2..418eafa153c 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -282,31 +282,31 @@ body {
&.ui-dark {
@include gitlab-theme(
- $theme-gray-200,
- $theme-gray-500,
- $theme-gray-700,
- $theme-gray-800,
- $theme-gray-900,
+ $gray-200,
+ $gray-500,
+ $gray-700,
+ $gray-800,
+ $gray-900,
$white-light
);
}
&.ui-light {
@include gitlab-theme(
- $theme-gray-700,
- $theme-gray-800,
- $theme-gray-700,
- $theme-gray-700,
- $theme-gray-100,
- $theme-gray-700
+ $gray-700,
+ $gray-800,
+ $gray-700,
+ $gray-700,
+ $gray-100,
+ $gray-700
);
.navbar-gitlab {
- background-color: $theme-gray-100;
+ background-color: $gray-100;
box-shadow: 0 1px 0 0 $border-color;
.logo-text svg {
- fill: $theme-gray-900;
+ fill: $gray-900;
}
.navbar-sub-nav,
@@ -315,7 +315,7 @@ body {
> a:hover,
> a:focus,
> button:hover {
- color: $theme-gray-900;
+ color: $gray-900;
}
&.active > a,
@@ -329,8 +329,8 @@ body {
.container-fluid {
.navbar-toggler,
.navbar-toggler:hover {
- color: $theme-gray-700;
- border-left: 1px solid $theme-gray-200;
+ color: $gray-700;
+ border-left: 1px solid $gray-200;
}
}
}
@@ -348,7 +348,7 @@ body {
.search-input-wrap {
.search-icon {
- fill: $theme-gray-200;
+ fill: $gray-200;
}
.search-input {
@@ -359,16 +359,16 @@ body {
.nav-sidebar li.active {
> a {
- color: $theme-gray-900;
+ color: $gray-900;
}
svg {
- fill: $theme-gray-900;
+ fill: $gray-900;
}
}
.sidebar-top-level-items > li.active .badge.badge-pill {
- color: $theme-gray-900;
+ color: $gray-900;
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 7d283dcfb71..f5ed6621c55 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -39,7 +39,6 @@
.header-content {
width: 100%;
- display: -webkit-flex;
display: flex;
justify-content: space-between;
position: relative;
@@ -47,11 +46,8 @@
padding-left: 0;
.title-container {
- display: -webkit-flex;
display: flex;
- -webkit-align-items: stretch;
align-items: stretch;
- -webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding-top: 0;
overflow: visible;
@@ -60,7 +56,6 @@
.title {
padding-right: 0;
color: currentColor;
- display: -webkit-flex;
display: flex;
position: relative;
margin: 0;
@@ -85,7 +80,6 @@
}
a {
- display: -webkit-flex;
display: flex;
align-items: center;
padding: 2px 8px;
@@ -173,7 +167,6 @@
.navbar-nav {
@include media-breakpoint-down(xs) {
- display: -webkit-flex;
display: flex;
padding-right: 10px;
flex-direction: row;
@@ -258,7 +251,6 @@
> li {
> a,
> button {
- display: -webkit-flex;
display: flex;
align-items: center;
justify-content: center;
@@ -294,7 +286,6 @@
}
.navbar-sub-nav {
- display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
@@ -326,14 +317,12 @@
}
.breadcrumbs {
- display: -webkit-flex;
display: flex;
min-height: $breadcrumb-min-height;
color: $gl-text-color;
}
.breadcrumbs-container {
- display: -webkit-flex;
display: flex;
width: 100%;
position: relative;
@@ -344,7 +333,6 @@
}
.breadcrumbs-links {
- -webkit-flex: 1;
flex: 1;
min-width: 0;
align-self: center;
@@ -379,7 +367,6 @@
}
.breadcrumbs-list {
- display: -webkit-flex;
display: flex;
margin-bottom: 0;
line-height: 16px;
@@ -430,7 +417,6 @@
}
.breadcrumbs-extra {
- display: -webkit-flex;
display: flex;
flex: 0 0 auto;
margin-left: auto;
@@ -565,15 +551,14 @@
}
.navbar-empty {
+ justify-content: center;
height: $header-height;
background: $white-light;
border-bottom: 1px solid $white-normal;
- .mx-auto {
- .tanuki-logo,
- img {
- height: 36px;
- }
+ .tanuki-logo,
+ .brand-header-logo {
+ max-height: 100%;
}
}
@@ -596,4 +581,16 @@
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
}
+
+ .input-group {
+ height: 34px;
+ }
+}
+
+.nav-links > li > a {
+ .badge.badge-pill {
+ @include media-breakpoint-down(xs) { display: none; }
+ }
+
+ @include media-breakpoint-down(xs) { margin-right: 3px; }
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 8db7d63266e..49b9b7014ae 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -87,8 +87,8 @@
display: flex;
align-items: center;
justify-content: center;
- border: $border-size solid $theme-gray-400;
+ border: $border-size solid $gray-400;
border-radius: 50%;
padding: $gl-padding-8 - $border-size;
- color: $theme-gray-700;
+ color: $gray-700;
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index a20920e2503..d78c707192f 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -38,7 +38,10 @@
svg {
fill: currentColor;
+}
+.square,
+svg {
$svg-sizes: 8 10 12 14 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index a66604e56ff..e51f230a680 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -45,9 +45,4 @@
&.status-box-upcoming {
background: $gl-text-color-secondary;
}
-
- &.status-box-milestone {
- color: $gl-text-color;
- background: $gray-darker;
- }
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 9218df9b40f..97cb9d90ff0 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -40,6 +40,14 @@ body {
.content {
margin: 0;
+
+ @include media-breakpoint-down(xs) { margin-top: 20px; }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .container .title {
+ padding-left: 15px !important;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d9d4a210f5f..1a74e06a75d 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -15,7 +15,7 @@
word-wrap: break-word;
&::after {
- content: " ";
+ content: ' ';
display: table;
clear: both;
}
@@ -167,7 +167,7 @@ ul.content-list {
}
.no-comments {
- opacity: .5;
+ opacity: 0.5;
}
}
@@ -196,8 +196,6 @@ ul.content-list {
// Content list using flexbox
.flex-list {
.flex-row {
- display: -webkit-flex;
- display: -ms-flexbox;
display: flex;
align-items: center;
white-space: nowrap;
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
index 429cfbe7235..c5feefb8c54 100644
--- a/app/assets/stylesheets/framework/logo.scss
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -9,7 +9,6 @@
}
.tanuki-logo {
-
.tanuki-left-ear,
.tanuki-right-ear,
.tanuki-nose {
@@ -34,7 +33,9 @@
.tanuki-left-cheek {
@include include-keyframes(animate-tanuki-left-cheek) {
- 0%, 10%, 100% {
+ 0%,
+ 10%,
+ 100% {
fill: lighten($tanuki-yellow, 25%);
}
@@ -46,11 +47,13 @@
.tanuki-left-eye {
@include include-keyframes(animate-tanuki-left-eye) {
- 10%, 80% {
+ 10%,
+ 80% {
fill: $tanuki-orange;
}
- 20%, 90% {
+ 20%,
+ 90% {
fill: lighten($tanuki-orange, 25%);
}
}
@@ -58,11 +61,13 @@
.tanuki-left-ear {
@include include-keyframes(animate-tanuki-left-ear) {
- 10%, 80% {
+ 10%,
+ 80% {
fill: $tanuki-red;
}
- 20%, 90% {
+ 20%,
+ 90% {
fill: lighten($tanuki-red, 25%);
}
}
@@ -70,11 +75,13 @@
.tanuki-nose {
@include include-keyframes(animate-tanuki-nose) {
- 20%, 70% {
+ 20%,
+ 70% {
fill: $tanuki-red;
}
- 30%, 80% {
+ 30%,
+ 80% {
fill: lighten($tanuki-red, 25%);
}
}
@@ -82,11 +89,13 @@
.tanuki-right-eye {
@include include-keyframes(animate-tanuki-right-eye) {
- 30%, 60% {
+ 30%,
+ 60% {
fill: $tanuki-orange;
}
- 40%, 70% {
+ 40%,
+ 70% {
fill: lighten($tanuki-orange, 25%);
}
}
@@ -94,11 +103,13 @@
.tanuki-right-ear {
@include include-keyframes(animate-tanuki-right-ear) {
- 30%, 60% {
+ 30%,
+ 60% {
fill: $tanuki-red;
}
- 40%, 70% {
+ 40%,
+ 70% {
fill: lighten($tanuki-red, 25%);
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5609a2086e6..d6c4e68f68f 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -149,16 +149,8 @@
margin: 10px 0;
}
- // Border around images in issue and MR comments.
- img:not(.emoji) {
- border: 1px solid $white-normal;
- padding: 5px;
- margin: 5px 0;
- // Ensure that image does not exceed viewport
- max-height: calc(100vh - 100px);
- }
- table {
+ table:not(.js-syntax-highlight) {
@include markdown-table;
}
}
@@ -173,7 +165,7 @@
svg {
width: 14px;
height: 14px;
- margin-top: 3px;
+ vertical-align: middle;
fill: $gl-text-color-secondary;
}
@@ -228,7 +220,7 @@
.cur {
.avatar {
- @include disableAllAnimation;
+ @include disable-all-animation;
border: 1px solid $white-light;
}
}
@@ -280,6 +272,8 @@
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
+ width: 100% !important;
+ font-family: $monospace-font !important;
}
.md-suggestion-header {
@@ -307,4 +301,8 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+ .referenced-users {
+ margin-right: 0;
+ }
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 9837b1a6bd0..ba3b0906e28 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -26,20 +26,11 @@
*/
@mixin markdown-table {
width: auto;
- display: inline-block;
+ display: block;
overflow-x: auto;
border: 0;
border-color: $gl-gray-100;
- @supports (width: fit-content) {
- display: block;
- width: fit-content;
- }
-
- tbody {
- background-color: $white-light;
- }
-
tr {
th {
border-bottom: solid 2px $gl-gray-100;
@@ -117,11 +108,6 @@
}
}
-@mixin dark-diff-match-line {
- color: $dark-diff-match-bg;
- background: $dark-diff-match-color;
-}
-
@mixin webkit-prefix($property, $value) {
#{'-webkit-' + $property}: $value;
#{$property}: $value;
@@ -129,16 +115,13 @@
/* http://phrappe.com/css/conditional-css-for-webkit-based-browsers/ */
@mixin on-webkit-only {
+ /* stylelint-disable-next-line media-feature-name-no-vendor-prefix */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@content;
}
}
@mixin keyframes($animation-name) {
- @-webkit-keyframes #{$animation-name} {
- @content;
- }
-
@keyframes #{$animation-name} {
@content;
}
@@ -178,12 +161,10 @@
width: 43px;
height: 30px;
transition-duration: 0.3s;
- -webkit-transform: translateZ(0);
- background: linear-gradient(
- to $gradient-direction,
- $gradient-color 45%,
- rgba($gradient-color, 0.4)
- );
+ transform: translateZ(0);
+ background: linear-gradient(to $gradient-direction,
+ $gradient-color 45%,
+ rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
@@ -279,7 +260,6 @@
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
- position: -webkit-sticky;
top: $header-height;
padding: $grid-size;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
deleted file mode 100644
index 3bb046d0e51..00000000000
--- a/app/assets/stylesheets/framework/mobile.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-/** Common mobile (screen XS, SM) styles **/
-@include media-breakpoint-down(xs) {
- .container .content {
- margin-top: 20px;
- }
-
- .nav-links > li > a {
- padding: 10px;
- font-size: 12px;
- margin-right: 3px;
-
- .badge.badge-pill {
- display: none;
- }
- }
-
- .referenced-users {
- margin-right: 0;
- }
-
- .issues-details-filters:not(.filtered-search-block),
- .dash-projects-filters,
- .check-all-holder {
- display: none;
- }
-
- .rss-btn {
- display: none;
- }
-
- .project-home-links {
- display: none;
- }
-
- .project-home-panel {
- padding-left: 0 !important;
-
- .project-repo-buttons,
- .git-clone-holder {
- display: none;
- }
- }
-
- .group-buttons {
- display: none;
- }
-
- .container .title {
- padding-left: 15px !important;
- }
-
- .nav-links,
- .nav-links {
- li a {
- font-size: 14px;
- padding: 19px 10px;
- }
- }
-
- .activity-filter-block {
- display: none;
- }
-
- .projects-search-form {
- .btn {
- display: none;
- }
- }
-}
-
-@include media-breakpoint-down(sm) {
- .issues-filters {
- .milestone-filter {
- display: none;
- }
- }
-
- .page-title {
- .note-created-ago,
- .new-issue-link {
- display: none;
- }
- }
-
- aside:not(.right-sidebar) {
- display: none;
- }
-}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 95291b4a9ad..3703b7568c8 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -29,10 +29,6 @@
padding-right: 28px;
}
}
-
- .page-title {
- margin-top: 0;
- }
}
.modal-body {
@@ -105,3 +101,42 @@ body.modal-open {
margin: 0;
}
}
+
+.issues-import-modal,
+.issues-export-modal {
+ .modal-header {
+ justify-content: flex-start;
+
+ .import-export-svg-container {
+ flex-grow: 1;
+ height: 56px;
+ padding: $gl-btn-padding $gl-btn-padding 0;
+ text-align: right;
+
+ .illustration {
+ height: inherit;
+ width: initial;
+ }
+ }
+ }
+
+ .modal-body {
+ padding: 0;
+
+ .modal-subheader {
+ justify-content: flex-start;
+ align-items: center;
+ border-bottom: 1px solid $modal-border-color;
+ padding: 14px;
+ }
+
+ .modal-text {
+ padding: $gl-padding-24 $gl-padding;
+ min-height: $modal-body-height;
+ }
+ }
+
+ .checkmark {
+ color: $green-400;
+ }
+}
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 29a9c076cdf..6bd44ee19bd 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -39,7 +39,7 @@
.table-section {
white-space: nowrap;
- $section-widths: 5 10 15 20 25 30 40 50 60 100;
+ $section-widths: 5 10 15 20 25 30 40 50 60 70 80 100;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index a68f1e4e570..ac673eafdc7 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -32,7 +32,7 @@
}
&::after {
- content: "\f078";
+ content: '\f078';
position: absolute;
z-index: 1;
text-align: center;
@@ -57,6 +57,16 @@
color: $gl-text-color;
}
}
+
+ &.is-invalid {
+ ~ .invalid-feedback {
+ display: block;
+ }
+
+ .select2-choices {
+ border-color: $red-500;
+ }
+ }
}
.select2-drop,
@@ -67,10 +77,18 @@
min-width: 175px;
color: $gl-text-color;
z-index: 999;
+
+ .modal-open & {
+ z-index: $zindex-modal + 200;
+ }
}
.select2-drop-mask {
z-index: 998;
+
+ .modal-open & {
+ z-index: $zindex-modal + 100;
+ }
}
.select2-drop.select2-drop-above.select2-drop-active {
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
new file mode 100644
index 00000000000..91fe75075dc
--- /dev/null
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -0,0 +1,51 @@
+@mixin spinner-color($color) {
+ border-color: rgba($color, 0.25);
+ border-top-color: $color;
+}
+
+@mixin spinner-size($size, $border-width) {
+ width: $size;
+ height: $size;
+ border-width: $border-width;
+ @include webkit-prefix(transform-origin, 50% 50% calc((#{$size} / 2) + #{$border-width}));
+}
+
+@keyframes spinner-rotate {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.spinner {
+ border-radius: 50%;
+ position: relative;
+ margin: 0 auto;
+ animation-name: spinner-rotate;
+ animation-duration: 0.6s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ border-style: solid;
+ display: inline-flex;
+ @include spinner-size(16px, 2px);
+ @include spinner-color($orange-600);
+
+ &.spinner-md {
+ @include spinner-size(32px, 3px);
+ }
+
+ &.spinner-lg {
+ @include spinner-size(64px, 4px);
+ }
+
+ &.spinner-dark {
+ @include spinner-color($gray-700);
+ }
+
+ &.spinner-light {
+ @include spinner-color($white);
+ }
+}
diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss
index 29a2d5881f7..2255d3be75b 100644
--- a/app/assets/stylesheets/framework/stacked_progress_bar.scss
+++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss
@@ -3,7 +3,7 @@
height: 16px;
border-radius: 10px;
overflow: hidden;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
.status-unavailable,
.status-green,
@@ -24,7 +24,7 @@
.status-unavailable {
padding: 0 10px;
- color: $theme-gray-700;
+ color: $gray-700;
}
.status-green {
@@ -36,11 +36,11 @@
}
.status-neutral {
- background-color: $theme-gray-200;
+ background-color: $gray-200;
color: $gl-gray-dark;
&:hover {
- background-color: $theme-gray-300;
+ background-color: $gray-300;
}
}
diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss
new file mode 100644
index 00000000000..3d66136938f
--- /dev/null
+++ b/app/assets/stylesheets/framework/system_messages.scss
@@ -0,0 +1,110 @@
+.header-message,
+.footer-message {
+ padding: 0 15px;
+ border: 1px solid transparent;
+ border-radius: 0;
+ position: fixed;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ margin: 0;
+ z-index: 1000;
+
+ p {
+ @include str-truncated(100%);
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
+.header-message {
+ top: 0;
+ height: $system-header-height;
+ line-height: $system-header-height;
+}
+
+.footer-message {
+ bottom: 0;
+ height: $system-footer-height;
+ line-height: $system-footer-height;
+}
+
+.with-performance-bar {
+ .header-message {
+ top: $performance-bar-height;
+ }
+}
+
+// System Header
+.with-system-header {
+ // main navigation
+ // login page
+ .navbar-gitlab,
+ .fixed-top {
+ top: $system-header-height;
+ }
+
+ // left sidebar eg: project page
+ // right sidebar eg: MR page
+ .nav-sidebar,
+ .right-sidebar {
+ top: $system-header-height + $header-height;
+ }
+
+ .content-wrapper {
+ margin-top: $system-header-height + $header-height;
+ }
+
+ // Performance Bar
+ // System Header
+ &.with-performance-bar {
+ // main navigation
+ header.navbar-gitlab {
+ top: $performance-bar-height + $system-header-height;
+ }
+
+ .layout-page {
+ margin-top: $header-height + $performance-bar-height + $system-header-height;
+ }
+
+ // left sidebar eg: project page
+ // right sidebar eg: MR page
+ .nav-sidebar,
+ .right-sidebar {
+ top: $header-height + $performance-bar-height + $system-header-height;
+ }
+ }
+}
+
+// System Footer
+.with-system-footer {
+ // left sidebar eg: project page
+ // right sidebar eg: mr page
+ .nav-sidebar,
+ .right-sidebar,
+ // navless pages' footer eg: login page
+ // navless pages' footer border eg: login page
+ &.devise-layout-html body .footer-container,
+ &.devise-layout-html body hr.footer-fixed {
+ bottom: $system-footer-height;
+ }
+}
+
+.fullscreen-layout {
+ .header-message,
+ .footer-message {
+ position: static;
+ top: auto;
+ bottom: auto;
+ }
+
+ .content-wrapper {
+ .with-system-header & {
+ margin-top: 0;
+ }
+
+ .with-system-footer & {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 6954e6599b1..295a5b5ee7a 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -50,6 +50,14 @@ table {
border-color: $white-normal;
}
}
+
+ .thead-white {
+ th {
+ background-color: $white-light;
+ color: $gl-text-color-secondary;
+ border-top: 0;
+ }
+ }
}
&.responsive-table {
@@ -153,3 +161,4 @@ table {
border-top: 0;
}
}
+
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
index 3f4be8829d7..b07d6023127 100644
--- a/app/assets/stylesheets/framework/terms.scss
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -13,7 +13,6 @@
.card {
.card-header {
- display: -webkit-flex;
display: flex;
align-items: center;
justify-content: space-between;
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 8258da07e4d..5f8ac3b7e37 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -34,7 +34,7 @@
background: $gl-gray-400;
border-radius: 12px;
padding: 3px;
- transition: all .4s ease;
+ transition: all 0.4s ease;
&::selection,
&::before::selection,
@@ -52,7 +52,7 @@
left: 0;
border-radius: 9px;
background: $feature-toggle-color;
- transition: all .2s ease;
+ transition: all 0.2s ease;
&,
.toggle-icon-svg {
@@ -135,12 +135,18 @@
}
@keyframes animate-enabled {
- 0%, 35% { opacity: 0; }
+ 0%,
+
+ 35% { opacity: 0; }
+
100% { opacity: 1; }
}
@keyframes animate-disabled {
- 0%, 35% { opacity: 0; }
+ 0%,
+
+ 35% { opacity: 0; }
+
100% { opacity: 1; }
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 0c81dc2e156..55ce0d7004e 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -2,7 +2,7 @@
color: $gl-text-color;
word-wrap: break-word;
- [dir="auto"] {
+ [dir='auto'] {
text-align: initial;
}
@@ -49,13 +49,6 @@
word-wrap: normal;
}
- // Multi-line code blocks should scroll horizontally
- pre {
- code {
- white-space: pre;
- }
- }
-
kbd {
display: inline-block;
padding: 3px 5px;
@@ -143,11 +136,12 @@
margin: 0 0 16px;
}
- table {
+ table:not(.js-syntax-highlight) {
@extend .table;
@extend .table-bordered;
margin: 16px 0;
color: $gl-text-color;
+ border: 0;
th {
background: $label-gray-bg;
@@ -165,6 +159,10 @@
overflow-x: auto;
border-radius: 2px;
+ // Multi-line code blocks should scroll horizontally
+ code {
+ white-space: pre;
+ }
&.plain-readme {
background: none;
@@ -226,8 +224,8 @@
}
}
- a[href*="/uploads/"],
- a[href*="storage.googleapis.com/google-code-attachments/"] {
+ a[href*='/uploads/'],
+ a[href*='storage.googleapis.com/google-code-attachments/'] {
&::before {
margin-right: 4px;
@@ -235,7 +233,7 @@
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
- content: "\f0c6";
+ content: '\f0c6';
}
&:hover::before {
@@ -302,11 +300,10 @@ body {
}
.page-title-empty {
- margin-top: 0;
+ margin: 12px 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: $gl-font-weight-bold;
- margin: 12px 0;
}
h1,
@@ -374,6 +371,16 @@ code {
.md:not(.use-csslab) {
@include md-typography;
+
+ &:not(.wiki) {
+ img:not(.emoji) {
+ border: 1px solid $white-normal;
+ padding: 5px;
+ margin: 5px 0;
+ // Ensure that image does not exceed viewport
+ max-height: calc(100vh - 100px);
+ }
+ }
}
/**
@@ -416,6 +423,7 @@ h4 {
/**
* form text input i.e. search bar, comments, forms, etc.
*/
+/* stylelint-disable selector-no-vendor-prefix */
input,
textarea {
&::-webkit-input-placeholder {
@@ -440,5 +448,6 @@ textarea {
color: $gl-text-color-tertiary;
}
}
+/* stylelint-enable */
.lh-100 { line-height: 1; }
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 343c09b4a3e..0333b9445c5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -32,13 +32,22 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
-$gl-gray-100: #dddddd;
-$gl-gray-200: #cccccc;
-$gl-gray-350: #aaaaaa;
-$gl-gray-400: #999999;
-$gl-gray-500: #777777;
-$gl-gray-600: #666666;
-$gl-gray-700: #555555;
+$black: #000;
+$black-transparent: rgba(0, 0, 0, 0.3);
+$almost-black: #242424;
+
+$t-gray-a-02: rgba($black, 0.02);
+$t-gray-a-04: rgba($black, 0.04);
+$t-gray-a-06: rgba($black, 0.06);
+$t-gray-a-08: rgba($black, 0.08);
+
+$gl-gray-100: #ddd;
+$gl-gray-200: #ccc;
+$gl-gray-350: #aaa;
+$gl-gray-400: #999;
+$gl-gray-500: #777;
+$gl-gray-600: #666;
+$gl-gray-700: #555;
$green-50: #f1fdf6;
$green-100: #dcf5e7;
@@ -88,6 +97,18 @@ $red-800: #8b2615;
$red-900: #711e11;
$red-950: #4b140b;
+$gray-50: #fafafa;
+$gray-100: #f2f2f2;
+$gray-200: #dfdfdf;
+$gray-300: #ccc;
+$gray-400: #bababa;
+$gray-500: #a7a7a7;
+$gray-600: #919191;
+$gray-700: #707070;
+$gray-800: #4f4f4f;
+$gray-900: #2e2e2e;
+$gray-950: #1f1f1f;
+
// GitLab themes
$indigo-50: #f7f7ff;
@@ -102,18 +123,6 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
-$theme-gray-50: #fafafa;
-$theme-gray-100: #f2f2f2;
-$theme-gray-200: #dfdfdf;
-$theme-gray-300: #cccccc;
-$theme-gray-400: #bababa;
-$theme-gray-500: #a7a7a7;
-$theme-gray-600: #919191;
-$theme-gray-700: #707070;
-$theme-gray-800: #4f4f4f;
-$theme-gray-900: #2e2e2e;
-$theme-gray-950: #1f1f1f;
-
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
$theme-blue-200: #c8d7e6;
@@ -170,11 +179,6 @@ $theme-light-red-500: #c24b38;
$theme-light-red-600: #b03927;
$theme-light-red-700: #a62e21;
-$black: #000;
-$black-transparent: rgba(0, 0, 0, 0.3);
-$shadow-color: rgba($black, 0.1);
-$almost-black: #242424;
-
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
@@ -187,6 +191,7 @@ $border-gray-dark: darken($white-normal, $darken-border-factor);
* UI elements
*/
$border-color: #e5e5e5;
+$shadow-color: $t-gray-a-08;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -198,7 +203,6 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
-$gl-font-size-medium: 20px;
$gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -239,6 +243,7 @@ $gl-padding-8: 8px;
$gl-padding: 16px;
$gl-padding-24: 24px;
$gl-padding-32: 32px;
+$gl-padding-50: 50px;
$gl-col-padding: 15px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
@@ -246,7 +251,7 @@ $gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
$input-horizontal-padding: 12px;
-$browserScrollbarSize: 10px;
+$browser-scrollbar-size: 10px;
/*
* Misc
@@ -260,6 +265,7 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px;
$border-radius-default: 4px;
$border-radius-small: 2px;
+$border-radius-large: 8px;
$default-icon-size: 18px;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
@@ -271,11 +277,13 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
+$system-header-height: 35px;
+$system-footer-height: $system-header-height;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
-$project-title-row-height: 64px;
-$project-avatar-mobile-size: 24px;
+$home-panel-title-row-height: 64px;
+$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
@@ -315,8 +323,8 @@ $line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
-$diff-image-info-color: gray;
-$diff-view-modes-color: gray;
+$diff-image-info-color: #808080;
+$diff-view-modes-color: #808080;
$diff-view-modes-border: #c1c1c1;
$diff-jagged-border-gradient-color: darken($white-normal, 8%);
@@ -335,6 +343,7 @@ $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-San
$dropdown-width: 300px;
$dropdown-min-height: 40px;
$dropdown-max-height: 312px;
+$dropdown-max-height-lg: 445px;
$dropdown-vertical-offset: 4px;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-shadow-color: rgba(#000, 0.1);
@@ -400,6 +409,8 @@ $status-icon-size: 22px;
$award-emoji-menu-shadow: rgba(0, 0, 0, 0.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
+$award-emoji-width: 376px;
+$award-emoji-width-xs: 300px;
/*
* Search Box
@@ -486,6 +497,7 @@ $builds-trace-bg: #111;
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
+$commit-stat-summary-height: 36px;
/*
* Common
@@ -652,6 +664,7 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
+$modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px;
@@ -659,3 +672,14 @@ $priority-label-empty-state-width: 114px;
Issues Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
+
+/*
+Merge Requests
+*/
+$mr-tabs-height: 51px;
+$mr-version-controls-height: 56px;
+
+/*
+Compare Branches
+*/
+$compare-branches-sticky-header-height: 68px;
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 069f45bff49..79d52932719 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -5,7 +5,7 @@
$secondary: $gray-light;
$input-disabled-bg: $gray-light;
-$input-border-color: $theme-gray-200;
+$input-border-color: $gray-200;
$input-color: $gl-text-color;
$font-family-sans-serif: $regular-font;
$font-family-monospace: $monospace-font;
@@ -20,7 +20,7 @@ $warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
-$dropdown-divider-bg: $theme-gray-200;
+$dropdown-divider-bg: $gray-200;
$dropdown-item-padding-y: 8px;
$dropdown-item-padding-x: 12px;
$popover-max-width: 300px;
@@ -34,3 +34,14 @@ $h3-font-size: 14px * 1.75;
$h4-font-size: 14px * 1.5;
$h5-font-size: 14px * 1.25;
$h6-font-size: 14px;
+$spacer: $grid-size;
+$spacers: (
+ 0: 0,
+ 1: ($spacer * 0.5),
+ 2: ($spacer),
+ 3: ($spacer * 2),
+ 4: ($spacer * 3),
+ 5: ($spacer * 4),
+ 6: ($spacer * 8)
+);
+$pagination-color: $gl-text-color;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
new file mode 100644
index 00000000000..ac3214a07d9
--- /dev/null
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -0,0 +1,18 @@
+@import '../framework/variables';
+
+@mixin diff-background($background, $idiff, $border) {
+ background: $background;
+
+ &.line_content span.idiff {
+ background: $idiff;
+ }
+
+ &.diff-line-num {
+ border-color: $border;
+ }
+}
+
+@mixin dark-diff-match-line {
+ color: $dark-diff-match-bg;
+ background: $dark-diff-match-color;
+}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
deleted file mode 100644
index 604f806dc58..00000000000
--- a/app/assets/stylesheets/highlight/dark.scss
+++ /dev/null
@@ -1,259 +0,0 @@
-/* https://github.com/MozMorris/tomorrow-pygments */
-
-/*
-* Dark syntax colors
-*/
-$dark-new-bg: rgba(51, 255, 51, 0.1);
-$dark-new-idiff: rgba(51, 255, 51, 0.2);
-$dark-old-bg: rgba(255, 51, 51, 0.2);
-$dark-old-idiff: rgba(255, 51, 51, 0.25);
-$dark-border: #808080;
-$dark-code-border: #666;
-$dark-main-bg: #1d1f21;
-$dark-main-color: #1d1f21;
-$dark-line-color: #c5c8c6;
-$dark-line-num-color: rgba(255, 255, 255, 0.3);
-$dark-line-num-color-new: #627165;
-$dark-line-num-color-old: #806565;
-$dark-diff-not-empty-bg: #557;
-$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;
-$dark-l: #de935f;
-$dark-n: #c5c8c6;
-$dark-o: #8abeb7;
-$dark-p: #c5c8c6;
-$dark-cm: #969896;
-$dark-cp: #969896;
-$dark-c1: #969896;
-$dark-cs: #969896;
-$dark-gd: #c66;
-$dark-gh: #c5c8c6;
-$dark-gi: #b5bd68;
-$dark-gp: #969896;
-$dark-gu: #8abeb7;
-$dark-kc: #b294bb;
-$dark-kd: #b294bb;
-$dark-kn: #8abeb7;
-$dark-kp: #b294bb;
-$dark-kr: #b294bb;
-$dark-kt: #f0c674;
-$dark-ld: #b5bd68;
-$dark-m: #de935f;
-$dark-s: #b5bd68;
-$dark-na: #81a2be;
-$dark-nb: #c5c8c6;
-$dark-nc: #f0c674;
-$dark-no: #c66;
-$dark-nd: #8abeb7;
-$dark-ni: #c5c8c6;
-$dark-ne: #c66;
-$dark-nf: #81a2be;
-$dark-nl: #c5c8c6;
-$dark-nn: #f0c674;
-$dark-nx: #81a2be;
-$dark-py: #c5c8c6;
-$dark-nt: #8abeb7;
-$dark-nv: #c66;
-$dark-ow: #8abeb7;
-$dark-w: #c5c8c6;
-$dark-mf: #de935f;
-$dark-mh: #de935f;
-$dark-mi: #de935f;
-$dark-mo: #de935f;
-$dark-sb: #b5bd68;
-$dark-sc: #c5c8c6;
-$dark-sd: #969896;
-$dark-s2: #b5bd68;
-$dark-se: #de935f;
-$dark-sh: #b5bd68;
-$dark-si: #de935f;
-$dark-sx: #b5bd68;
-$dark-sr: #b5bd68;
-$dark-s1: #b5bd68;
-$dark-ss: #b5bd68;
-$dark-bp: #c5c8c6;
-$dark-vc: #c66;
-$dark-vg: #c66;
-$dark-vi: #c66;
-$dark-il: #de935f;
-
-.code.dark {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $dark-main-bg;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $dark-line-num-color;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $dark-code-border;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $dark-main-bg;
- color: $dark-line-color;
- }
-
- // Diff line
- .line_holder {
- &.match .line_content,
- &.old-nonewline .line_content,
- &.new-nonewline .line_content {
- @include dark-diff-match-line;
- }
-
- td.diff-line-num.hll:not(.empty-cell),
- td.line_content.hll:not(.empty-cell) {
- background-color: $dark-diff-not-empty-bg;
- border-color: darken($dark-diff-not-empty-bg, 15%);
- }
-
- .diff-line-num.new,
- .line_content.new {
- @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
-
- &::before,
- a {
- color: $dark-line-num-color-new;
- }
- }
-
- .diff-line-num.old,
- .line_content.old {
- @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
-
- &::before,
- a {
- color: $dark-line-num-color-old;
- }
- }
-
- .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
- pre .hll {
- background-color: $dark-pre-hll-bg !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $dark-highlight-bg !important;
- color: $dark-highlight-color !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $dark-na;
- }
-
- .hll { background-color: $dark-hll-bg; }
- .c { color: $dark-c; } /* Comment */
- .err { color: $dark-err; } /* Error */
- .k { color: $dark-k; } /* Keyword */
- .l { color: $dark-l; } /* Literal */
- .n { color: $dark-n; } /* Name */
- .o { color: $dark-o; } /* Operator */
- .p { color: $dark-p; } /* Punctuation */
- .cm { color: $dark-cm; } /* Comment.Multiline */
- .cp { color: $dark-cp; } /* Comment.Preproc */
- .c1 { color: $dark-c1; } /* Comment.Single */
- .cs { color: $dark-cs; } /* Comment.Special */
- .gd { color: $dark-gd; } /* Generic.Deleted */
- .ge { font-style: italic; } /* Generic.Emph */
- .gh { color: $dark-gh; font-weight: $gl-font-weight-bold; } /* Generic.Heading */
- .gi { color: $dark-gi; } /* Generic.Inserted */
- .gp { color: $dark-gp; font-weight: $gl-font-weight-bold; } /* Generic.Prompt */
- .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
- .gu { color: $dark-gu; font-weight: $gl-font-weight-bold; } /* Generic.Subheading */
- .kc { color: $dark-kc; } /* Keyword.Constant */
- .kd { color: $dark-kd; } /* Keyword.Declaration */
- .kn { color: $dark-kn; } /* Keyword.Namespace */
- .kp { color: $dark-kp; } /* Keyword.Pseudo */
- .kr { color: $dark-kr; } /* Keyword.Reserved */
- .kt { color: $dark-kt; } /* Keyword.Type */
- .ld { color: $dark-ld; } /* Literal.Date */
- .m { color: $dark-m; } /* Literal.Number */
- .s { color: $dark-s; } /* Literal.String */
- .na { color: $dark-na; } /* Name.Attribute */
- .nb { color: $dark-nb; } /* Name.Builtin */
- .nc { color: $dark-nc; } /* Name.Class */
- .no { color: $dark-no; } /* Name.Constant */
- .nd { color: $dark-nd; } /* Name.Decorator */
- .ni { color: $dark-ni; } /* Name.Entity */
- .ne { color: $dark-ne; } /* Name.Exception */
- .nf { color: $dark-nf; } /* Name.Function */
- .nl { color: $dark-nl; } /* Name.Label */
- .nn { color: $dark-nn; } /* Name.Namespace */
- .nx { color: $dark-nx; } /* Name.Other */
- .py { color: $dark-py; } /* Name.Property */
- .nt { color: $dark-nt; } /* Name.Tag */
- .nv { color: $dark-nv; } /* Name.Variable */
- .ow { color: $dark-ow; } /* Operator.Word */
- .w { color: $dark-w; } /* Text.Whitespace */
- .mf { color: $dark-mf; } /* Literal.Number.Float */
- .mh { color: $dark-mh; } /* Literal.Number.Hex */
- .mi { color: $dark-mi; } /* Literal.Number.Integer */
- .mo { color: $dark-mo; } /* Literal.Number.Oct */
- .sb { color: $dark-sb; } /* Literal.String.Backtick */
- .sc { color: $dark-sc; } /* Literal.String.Char */
- .sd { color: $dark-sd; } /* Literal.String.Doc */
- .s2 { color: $dark-s2; } /* Literal.String.Double */
- .se { color: $dark-se; } /* Literal.String.Escape */
- .sh { color: $dark-sh; } /* Literal.String.Heredoc */
- .si { color: $dark-si; } /* Literal.String.Interpol */
- .sx { color: $dark-sx; } /* Literal.String.Other */
- .sr { color: $dark-sr; } /* Literal.String.Regex */
- .s1 { color: $dark-s1; } /* Literal.String.Single */
- .ss { color: $dark-ss; } /* Literal.String.Symbol */
- .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */
- .vc { color: $dark-vc; } /* Name.Variable.Class */
- .vg { color: $dark-vg; } /* Name.Variable.Global */
- .vi { color: $dark-vi; } /* Name.Variable.Instance */
- .il { color: $dark-il; } /* Literal.Number.Integer.Long */
-}
diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss
index 44c8a1d39ec..74364ee4ddb 100644
--- a/app/assets/stylesheets/highlight/embedded.scss
+++ b/app/assets/stylesheets/highlight/embedded.scss
@@ -1,3 +1,3 @@
.code {
- @import "white_base";
+ @import 'white_base';
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
deleted file mode 100644
index 8e2720511da..00000000000
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ /dev/null
@@ -1,257 +0,0 @@
-/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
-
-/*
-* Monokai Colors
-*/
-$monokai-bg: #272822;
-$monokai-border: #555;
-$monokai-text-color: #f8f8f2;
-$monokai-line-num-color: rgba(255, 255, 255, 0.3);
-$monokai-line-num-color-new: #707565;
-$monokai-line-num-color-old: #7e736f;
-$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);
-
-$monokai-old-bg: rgba(254, 147, 140, 0.15);
-$monokai-old-idiff: rgba(254, 147, 140, 0.2);
-
-$monokai-hll: #49483e;
-$monokai-c: #75715e;
-$monokai-err-color: #960050;
-$monokai-err-bg: #1e0010;
-$monokai-k: #66d9ef;
-$monokai-l: #ae81ff;
-$monokai-n: #f8f8f2;
-$monokai-o: #f92672;
-$monokai-p: #f8f8f2;
-$monokai-cm: #75715e;
-$monokai-cp: #75715e;
-$monokai-c1: #75715e;
-$monokai-cs: #75715e;
-$monokai-kc: #66d9ef;
-$monokai-kd: #66d9ef;
-$monokai-kn: #f92672;
-$monokai-kp: #66d9ef;
-$monokai-kr: #66d9ef;
-$monokai-kt: #66d9ef;
-$monokai-ld: #e6db74;
-$monokai-m: #ae81ff;
-$monokai-s: #e6db74;
-$monokai-na: #a6e22e;
-$monokai-nb: #f8f8f2;
-$monokai-nc: #a6e22e;
-$monokai-no: #66d9ef;
-$monokai-nd: #a6e22e;
-$monokai-ni: #f8f8f2;
-$monokai-ne: #a6e22e;
-$monokai-nf: #a6e22e;
-$monokai-nl: #f8f8f2;
-$monokai-nn: #f8f8f2;
-$monokai-nx: #a6e22e;
-$monokai-py: #f8f8f2;
-$monokai-nt: #f92672;
-$monokai-nv: #f8f8f2;
-$monokai-ow: #f92672;
-$monokai-w: #f8f8f2;
-$monokai-mf: #ae81ff;
-$monokai-mh: #ae81ff;
-$monokai-mi: #ae81ff;
-$monokai-mo: #ae81ff;
-$monokai-sb: #e6db74;
-$monokai-sc: #e6db74;
-$monokai-sd: #e6db74;
-$monokai-s2: #e6db74;
-$monokai-se: #ae81ff;
-$monokai-sh: #e6db74;
-$monokai-si: #e6db74;
-$monokai-sx: #e6db74;
-$monokai-sr: #e6db74;
-$monokai-s1: #e6db74;
-$monokai-ss: #e6db74;
-$monokai-bp: #f8f8f2;
-$monokai-vc: #f8f8f2;
-$monokai-vg: #f8f8f2;
-$monokai-vi: #f8f8f2;
-$monokai-il: #ae81ff;
-$monokai-gu: #75715e;
-$monokai-gd: #f92672;
-$monokai-gi: #a6e22e;
-
-.code.monokai {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $monokai-bg;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $monokai-line-num-color;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $monokai-border;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $monokai-bg;
- color: $monokai-text-color;
- }
-
- // Diff line
- .line_holder {
- &.match .line_content,
- &.old-nonewline .line_content,
- &.new-nonewline .line_content {
- @include dark-diff-match-line;
- }
-
- td.diff-line-num.hll:not(.empty-cell),
- td.line_content.hll:not(.empty-cell) {
- background-color: $monokai-line-empty-bg;
- border-color: $monokai-line-empty-border;
- }
-
- .diff-line-num.new,
- .line_content.new {
- @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
-
- &::before,
- a {
- color: $monokai-line-num-color-new;
- }
- }
-
- .diff-line-num.old,
- .line_content.old {
- @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
-
- &::before,
- a {
- color: $monokai-line-num-color-old;
- }
- }
-
- .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
- pre .hll {
- background-color: $monokai-hll !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $monokai-highlight-bg !important;
- color: $black !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $monokai-k;
- }
-
- .hll { background-color: $monokai-hll; }
- .c { color: $monokai-c; } /* Comment */
- .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */
- .k { color: $monokai-k; } /* Keyword */
- .l { color: $monokai-l; } /* Literal */
- .n { color: $monokai-n; } /* Name */
- .o { color: $monokai-o; } /* Operator */
- .p { color: $monokai-p; } /* Punctuation */
- .cm { color: $monokai-cm; } /* Comment.Multiline */
- .cp { color: $monokai-cp; } /* Comment.Preproc */
- .c1 { color: $monokai-c1; } /* Comment.Single */
- .cs { color: $monokai-cs; } /* Comment.Special */
- .ge { font-style: italic; } /* Generic.Emph */
- .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
- .kc { color: $monokai-kc; } /* Keyword.Constant */
- .kd { color: $monokai-kd; } /* Keyword.Declaration */
- .kn { color: $monokai-kn; } /* Keyword.Namespace */
- .kp { color: $monokai-kp; } /* Keyword.Pseudo */
- .kr { color: $monokai-kr; } /* Keyword.Reserved */
- .kt { color: $monokai-kt; } /* Keyword.Type */
- .ld { color: $monokai-ld; } /* Literal.Date */
- .m { color: $monokai-m; } /* Literal.Number */
- .s { color: $monokai-s; } /* Literal.String */
- .na { color: $monokai-na; } /* Name.Attribute */
- .nb { color: $monokai-nb; } /* Name.Builtin */
- .nc { color: $monokai-nc; } /* Name.Class */
- .no { color: $monokai-no; } /* Name.Constant */
- .nd { color: $monokai-nd; } /* Name.Decorator */
- .ni { color: $monokai-ni; } /* Name.Entity */
- .ne { color: $monokai-ne; } /* Name.Exception */
- .nf { color: $monokai-nf; } /* Name.Function */
- .nl { color: $monokai-nl; } /* Name.Label */
- .nn { color: $monokai-nn; } /* Name.Namespace */
- .nx { color: $monokai-nx; } /* Name.Other */
- .py { color: $monokai-py; } /* Name.Property */
- .nt { color: $monokai-nt; } /* Name.Tag */
- .nv { color: $monokai-nv; } /* Name.Variable */
- .ow { color: $monokai-ow; } /* Operator.Word */
- .w { color: $monokai-w; } /* Text.Whitespace */
- .mf { color: $monokai-mf; } /* Literal.Number.Float */
- .mh { color: $monokai-mh; } /* Literal.Number.Hex */
- .mi { color: $monokai-mi; } /* Literal.Number.Integer */
- .mo { color: $monokai-mo; } /* Literal.Number.Oct */
- .sb { color: $monokai-sb; } /* Literal.String.Backtick */
- .sc { color: $monokai-sc; } /* Literal.String.Char */
- .sd { color: $monokai-sd; } /* Literal.String.Doc */
- .s2 { color: $monokai-s2; } /* Literal.String.Double */
- .se { color: $monokai-se; } /* Literal.String.Escape */
- .sh { color: $monokai-sh; } /* Literal.String.Heredoc */
- .si { color: $monokai-si; } /* Literal.String.Interpol */
- .sx { color: $monokai-sx; } /* Literal.String.Other */
- .sr { color: $monokai-sr; } /* Literal.String.Regex */
- .s1 { color: $monokai-s1; } /* Literal.String.Single */
- .ss { color: $monokai-ss; } /* Literal.String.Symbol */
- .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */
- .vc { color: $monokai-vc; } /* Name.Variable.Class */
- .vg { color: $monokai-vg; } /* Name.Variable.Global */
- .vi { color: $monokai-vi; } /* Name.Variable.Instance */
- .il { color: $monokai-il; } /* Literal.Number.Integer.Long */
- .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */
- .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */
- .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */
-}
diff --git a/app/assets/stylesheets/highlight/none.scss b/app/assets/stylesheets/highlight/none.scss
deleted file mode 100644
index 7d692a87e33..00000000000
--- a/app/assets/stylesheets/highlight/none.scss
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
-* None Syntax Colors
-*/
-
-
-
-@mixin matchLine {
- color: $black-transparent;
- background-color: $white-normal;
-}
-
-.code.none {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $gray-light;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $black-transparent;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $white-normal;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $white-light;
- color: $gl-text-color;
- }
-
-// Diff line
-
- $none-over-bg: #ded7fc;
- $none-expanded-border: #e0e0e0;
- $none-expanded-bg: #f7f7f7;
-
- .line_holder {
-
- &.match .line_content,
- .new-nonewline.line_content,
- .old-nonewline.line_content {
- @include matchLine;
- }
-
- .diff-line-num {
- &.old {
- background-color: $line-number-old;
- border-color: $line-removed-dark;
-
- a {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.new {
- background-color: $line-number-new;
- border-color: $line-added-dark;
-
- a {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
- }
-
- &.is-over,
- &.hll:not(.empty-cell).is-over {
- background-color: $none-over-bg;
- border-color: darken($none-over-bg, 5%);
-
- a {
- color: darken($none-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 $none-expanded-border;
- }
- }
-
- &.diff-expanded {
- > .diff-line-num,
- > .line_content {
- background: $none-expanded-bg;
- border-color: $none-expanded-bg;
- }
- }
-
- .line_content {
- &.old {
- background-color: $line-removed;
-
- &::before {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-removed-dark;
- }
- }
-
- &.new {
- background-color: $line-added;
-
- &::before {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
- }
-
- span.idiff {
- background-color: $line-added-dark;
- }
- }
-
- &.match {
- @include matchLine;
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-select-yellow;
- }
- }
- }
-
- // highlight line via anchor
- pre .hll {
- background-color: $white-normal;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $white-normal;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $gl-text-color;
- text-decoration: underline;
- }
-
- .hll { background-color: $white-light; }
-
- .gd {
- color: $gl-text-color;
- background-color: $white-light;
-
- .x {
- color: $gl-text-color;
- background-color: $white-light;
- }
- }
-
- .gi {
- color: $gl-text-color;
- background-color: $white-light;
-
- .x {
- color: $gl-text-color;
- background-color: $white-light;
- }
- }
-
- .c { color: $gl-text-color; } /* Comment */
- .err { color: $gl-text-color; } /* Error */
- .g { color: $gl-text-color; } /* Generic */
- .k { color: $gl-text-color; } /* Keyword */
- .l { color: $gl-text-color; } /* Literal */
- .n { color: $gl-text-color; } /* Name */
- .o { color: $gl-text-color; } /* Operator */
- .x { color: $gl-text-color; } /* Other */
- .p { color: $gl-text-color; } /* Punctuation */
- .cm { color: $gl-text-color; } /* Comment.Multiline */
- .cp { color: $gl-text-color; } /* Comment.Preproc */
- .c1 { color: $gl-text-color; } /* Comment.Single */
- .cs { color: $gl-text-color; } /* Comment.Special */
- .ge { color: $gl-text-color; } /* Generic.Emph */
- .gr { color: $gl-text-color; } /* Generic.Error */
- .gh { color: $gl-text-color; } /* Generic.Heading */
- .go { color: $gl-text-color; } /* Generic.Output */
- .gp { color: $gl-text-color; } /* Generic.Prompt */
- .gs { color: $gl-text-color; } /* Generic.Strong */
- .gu { color: $gl-text-color; } /* Generic.Subheading */
- .gt { color: $gl-text-color; } /* Generic.Traceback */
- .kc { color: $gl-text-color; } /* Keyword.Constant */
- .kd { color: $gl-text-color; } /* Keyword.Declaration */
- .kn { color: $gl-text-color; } /* Keyword.Namespace */
- .kp { color: $gl-text-color; } /* Keyword.Pseudo */
- .kr { color: $gl-text-color; } /* Keyword.Reserved */
- .kt { color: $gl-text-color; } /* Keyword.Type */
- .ld { color: $gl-text-color; } /* Literal.Date */
- .m { color: $gl-text-color; } /* Literal.Number */
- .s { color: $gl-text-color; } /* Literal.String */
- .na { color: $gl-text-color; } /* Name.Attribute */
- .nb { color: $gl-text-color; } /* Name.Builtin */
- .nc { color: $gl-text-color; } /* Name.Class */
- .no { color: $gl-text-color; } /* Name.Constant */
- .nd { color: $gl-text-color; } /* Name.Decorator */
- .ni { color: $gl-text-color; } /* Name.Entity */
- .ne { color: $gl-text-color; } /* Name.Exception */
- .nf { color: $gl-text-color; } /* Name.Function */
- .nl { color: $gl-text-color; } /* Name.Label */
- .nn { color: $gl-text-color; } /* Name.Namespace */
- .nx { color: $gl-text-color; } /* Name.Other */
- .py { color: $gl-text-color; } /* Name.Property */
- .nt { color: $gl-text-color; } /* Name.Tag */
- .nv { color: $gl-text-color; } /* Name.Variable */
- .ow { color: $gl-text-color; } /* Operator.Word */
- .w { color: $gl-text-color; } /* Text.Whitespace */
- .mf { color: $gl-text-color; } /* Literal.Number.Float */
- .mh { color: $gl-text-color; } /* Literal.Number.Hex */
- .mi { color: $gl-text-color; } /* Literal.Number.Integer */
- .mo { color: $gl-text-color; } /* Literal.Number.Oct */
- .sb { color: $gl-text-color; } /* Literal.String.Backtick */
- .sc { color: $gl-text-color; } /* Literal.String.Char */
- .sd { color: $gl-text-color; } /* Literal.String.Doc */
- .s2 { color: $gl-text-color; } /* Literal.String.Double */
- .se { color: $gl-text-color; } /* Literal.String.Escape */
- .sh { color: $gl-text-color; } /* Literal.String.Heredoc */
- .si { color: $gl-text-color; } /* Literal.String.Interpol */
- .sx { color: $gl-text-color; } /* Literal.String.Other */
- .sr { color: $gl-text-color; } /* Literal.String.Regex */
- .s1 { color: $gl-text-color; } /* Literal.String.Single */
- .ss { color: $gl-text-color; } /* Literal.String.Symbol */
- .bp { color: $gl-text-color; } /* Name.Builtin.Pseudo */
- .vc { color: $gl-text-color; } /* Name.Variable.Class */
- .vg { color: $gl-text-color; } /* Name.Variable.Global */
- .vi { color: $gl-text-color; } /* Name.Variable.Instance */
- .il { color: $gl-text-color; } /* Literal.Number.Integer.Long */
-
-}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
deleted file mode 100644
index cd1f0f6650f..00000000000
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ /dev/null
@@ -1,284 +0,0 @@
-/* https://gist.github.com/qguv/7936275 */
-
-/*
-* Solarized dark colors
-*/
-$solarized-dark-new-bg: rgba(133, 153, 0, 0.15);
-$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25);
-$solarized-dark-old-bg: rgba(220, 50, 47, 0.3);
-$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25);
-$solarized-dark-border: #113b46;
-$solarized-dark-pre-bg: #002b36;
-$solarized-dark-pre-color: #93a1a1;
-$solarized-dark-pre-border: #113b46;
-$solarized-dark-line-bg: #002b36;
-$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
-$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;
-$solarized-dark-k: #859900;
-$solarized-dark-l: #93a1a1;
-$solarized-dark-n: #93a1a1;
-$solarized-dark-o: #859900;
-$solarized-dark-x: #cb4b16;
-$solarized-dark-p: #93a1a1;
-$solarized-dark-cm: #586e75;
-$solarized-dark-cp: #859900;
-$solarized-dark-c1: #586e75;
-$solarized-dark-cs: #859900;
-$solarized-dark-gd: #2aa198;
-$solarized-dark-ge: #93a1a1;
-$solarized-dark-gr: #dc322f;
-$solarized-dark-gh: #cb4b16;
-$solarized-dark-gi: #859900;
-$solarized-dark-go: #93a1a1;
-$solarized-dark-gp: #93a1a1;
-$solarized-dark-gs: #93a1a1;
-$solarized-dark-gu: #cb4b16;
-$solarized-dark-gt: #93a1a1;
-$solarized-dark-kc: #cb4b16;
-$solarized-dark-kd: #268bd2;
-$solarized-dark-kn: #859900;
-$solarized-dark-kp: #859900;
-$solarized-dark-kr: #268bd2;
-$solarized-dark-kt: #dc322f;
-$solarized-dark-ld: #93a1a1;
-$solarized-dark-m: #2aa198;
-$solarized-dark-s: #2aa198;
-$solarized-dark-na: #93a1a1;
-$solarized-dark-nb: #b58900;
-$solarized-dark-nc: #268bd2;
-$solarized-dark-no: #cb4b16;
-$solarized-dark-nd: #268bd2;
-$solarized-dark-ni: #cb4b16;
-$solarized-dark-ne: #cb4b16;
-$solarized-dark-nf: #268bd2;
-$solarized-dark-nl: #93a1a1;
-$solarized-dark-nn: #93a1a1;
-$solarized-dark-nx: #93a1a1;
-$solarized-dark-py: #93a1a1;
-$solarized-dark-nt: #268bd2;
-$solarized-dark-nv: #268bd2;
-$solarized-dark-ow: #859900;
-$solarized-dark-w: #93a1a1;
-$solarized-dark-mf: #2aa198;
-$solarized-dark-mh: #2aa198;
-$solarized-dark-mi: #2aa198;
-$solarized-dark-mo: #2aa198;
-$solarized-dark-sb: #586e75;
-$solarized-dark-sc: #2aa198;
-$solarized-dark-sd: #93a1a1;
-$solarized-dark-s2: #2aa198;
-$solarized-dark-se: #cb4b16;
-$solarized-dark-sh: #93a1a1;
-$solarized-dark-si: #2aa198;
-$solarized-dark-sx: #2aa198;
-$solarized-dark-sr: #dc322f;
-$solarized-dark-s1: #2aa198;
-$solarized-dark-ss: #2aa198;
-$solarized-dark-bp: #268bd2;
-$solarized-dark-vc: #268bd2;
-$solarized-dark-vg: #268bd2;
-$solarized-dark-vi: #268bd2;
-$solarized-dark-il: #2aa198;
-
-.code.solarized-dark {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $solarized-dark-line-bg;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $solarized-dark-line-color;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $solarized-dark-pre-border;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $solarized-dark-pre-bg;
- color: $solarized-dark-pre-color;
- }
-
- // Diff line
- .line_holder {
- &.match .line_content,
- &.old-nonewline .line_content,
- &.new-nonewline .line_content {
- @include dark-diff-match-line;
- }
-
- td.diff-line-num.hll:not(.empty-cell),
- td.line_content.hll:not(.empty-cell) {
- background-color: $solarized-dark-hll-bg;
- border-color: darken($solarized-dark-hll-bg, 15%);
- }
-
- .diff-line-num.new,
- .line_content.new {
- @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
-
- &::before,
- a {
- color: $solarized-dark-line-color-new;
- }
- }
-
- .diff-line-num.old,
- .line_content.old {
- @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
-
- &::before,
- a {
- color: $solarized-dark-line-color-old;
- }
- }
-
- .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
- pre .hll {
- background-color: $solarized-dark-hll-bg !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $solarized-dark-highlight !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $solarized-dark-kd;
- }
-
- /* Solarized Dark
-
- For use with Jekyll and Pygments
-
- http://ethanschoonover.com/solarized
-
- SOLARIZED HEX ROLE
- --------- -------- ------------------------------------------
- base03 #002b36 background
- base01 #586e75 comments / secondary content
- base1 #93a1a1 body text / default code / primary content
- orange #cb4b16 constants
- red #dc322f regex, special keywords
- blue #268bd2 reserved keywords
- cyan #2aa198 strings, numbers
- green #859900 operators, other keywords
- */
-
- .c { color: $solarized-dark-c; } /* Comment */
- .err { color: $solarized-dark-err; } /* Error */
- .g { color: $solarized-dark-g; } /* Generic */
- .k { color: $solarized-dark-k; } /* Keyword */
- .l { color: $solarized-dark-l; } /* Literal */
- .n { color: $solarized-dark-n; } /* Name */
- .o { color: $solarized-dark-o; } /* Operator */
- .x { color: $solarized-dark-x; } /* Other */
- .p { color: $solarized-dark-p; } /* Punctuation */
- .cm { color: $solarized-dark-cm; } /* Comment.Multiline */
- .cp { color: $solarized-dark-cp; } /* Comment.Preproc */
- .c1 { color: $solarized-dark-c1; } /* Comment.Single */
- .cs { color: $solarized-dark-cs; } /* Comment.Special */
- .gd { color: $solarized-dark-gd; } /* Generic.Deleted */
- .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */
- .gr { color: $solarized-dark-gr; } /* Generic.Error */
- .gh { color: $solarized-dark-gh; } /* Generic.Heading */
- .gi { color: $solarized-dark-gi; } /* Generic.Inserted */
- .go { color: $solarized-dark-go; } /* Generic.Output */
- .gp { color: $solarized-dark-gp; } /* Generic.Prompt */
- .gs { color: $solarized-dark-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
- .gu { color: $solarized-dark-gu; } /* Generic.Subheading */
- .gt { color: $solarized-dark-gt; } /* Generic.Traceback */
- .kc { color: $solarized-dark-kc; } /* Keyword.Constant */
- .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */
- .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */
- .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */
- .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */
- .kt { color: $solarized-dark-kt; } /* Keyword.Type */
- .ld { color: $solarized-dark-ld; } /* Literal.Date */
- .m { color: $solarized-dark-m; } /* Literal.Number */
- .s { color: $solarized-dark-s; } /* Literal.String */
- .na { color: $solarized-dark-na; } /* Name.Attribute */
- .nb { color: $solarized-dark-nb; } /* Name.Builtin */
- .nc { color: $solarized-dark-nc; } /* Name.Class */
- .no { color: $solarized-dark-no; } /* Name.Constant */
- .nd { color: $solarized-dark-nd; } /* Name.Decorator */
- .ni { color: $solarized-dark-ni; } /* Name.Entity */
- .ne { color: $solarized-dark-ne; } /* Name.Exception */
- .nf { color: $solarized-dark-nf; } /* Name.Function */
- .nl { color: $solarized-dark-nl; } /* Name.Label */
- .nn { color: $solarized-dark-nn; } /* Name.Namespace */
- .nx { color: $solarized-dark-nx; } /* Name.Other */
- .py { color: $solarized-dark-py; } /* Name.Property */
- .nt { color: $solarized-dark-nt; } /* Name.Tag */
- .nv { color: $solarized-dark-nv; } /* Name.Variable */
- .ow { color: $solarized-dark-ow; } /* Operator.Word */
- .w { color: $solarized-dark-w; } /* Text.Whitespace */
- .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */
- .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */
- .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */
- .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */
- .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */
- .sc { color: $solarized-dark-sc; } /* Literal.String.Char */
- .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */
- .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */
- .se { color: $solarized-dark-se; } /* Literal.String.Escape */
- .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */
- .si { color: $solarized-dark-si; } /* Literal.String.Interpol */
- .sx { color: $solarized-dark-sx; } /* Literal.String.Other */
- .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */
- .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */
- .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */
- .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */
- .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */
- .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */
- .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */
- .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */
-}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
deleted file mode 100644
index 09c3ea36414..00000000000
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ /dev/null
@@ -1,292 +0,0 @@
-/* https://gist.github.com/qguv/7936275 */
-
-/*
-* Solarized light syntax colors
-*/
-$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4);
-$solarized-light-new-bg: rgba(133, 153, 0, 0.2);
-$solarized-light-new-idiff: rgba(133, 153, 0, 0.25);
-$solarized-light-old-bg: rgba(220, 50, 47, 0.2);
-$solarized-light-old-idiff: rgba(220, 50, 47, 0.25);
-$solarized-light-border: #c5d0d4;
-$solarized-light-pre-bg: #002b36;
-$solarized-light-pre-bg: #fdf6e3;
-$solarized-light-pre-color: #586e75;
-$solarized-light-line-bg: #fdf6e3;
-$solarized-light-line-color: rgba(0, 0, 0, 0.3);
-$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;
-$solarized-light-k: #859900;
-$solarized-light-l: #586e75;
-$solarized-light-n: #586e75;
-$solarized-light-o: #859900;
-$solarized-light-x: #cb4b16;
-$solarized-light-p: #586e75;
-$solarized-light-cm: #93a1a1;
-$solarized-light-cp: #859900;
-$solarized-light-c1: #93a1a1;
-$solarized-light-cs: #859900;
-$solarized-light-gd: #2aa198;
-$solarized-light-ge: #586e75;
-$solarized-light-gr: #dc322f;
-$solarized-light-gh: #cb4b16;
-$solarized-light-gi: #859900;
-$solarized-light-go: #586e75;
-$solarized-light-gp: #586e75;
-$solarized-light-gs: #586e75;
-$solarized-light-gu: #cb4b16;
-$solarized-light-gt: #586e75;
-$solarized-light-kc: #cb4b16;
-$solarized-light-kd: #268bd2;
-$solarized-light-kn: #859900;
-$solarized-light-kp: #859900;
-$solarized-light-kr: #268bd2;
-$solarized-light-kt: #dc322f;
-$solarized-light-ld: #586e75;
-$solarized-light-m: #2aa198;
-$solarized-light-s: #2aa198;
-$solarized-light-na: #586e75;
-$solarized-light-nb: #b58900;
-$solarized-light-nc: #268bd2;
-$solarized-light-no: #cb4b16;
-$solarized-light-nd: #268bd2;
-$solarized-light-ni: #cb4b16;
-$solarized-light-ne: #cb4b16;
-$solarized-light-nf: #268bd2;
-$solarized-light-nl: #586e75;
-$solarized-light-nn: #586e75;
-$solarized-light-nx: #586e75;
-$solarized-light-py: #586e75;
-$solarized-light-nt: #268bd2;
-$solarized-light-nv: #268bd2;
-$solarized-light-ow: #859900;
-$solarized-light-w: #586e75;
-$solarized-light-mf: #2aa198;
-$solarized-light-mh: #2aa198;
-$solarized-light-mi: #2aa198;
-$solarized-light-mo: #2aa198;
-$solarized-light-sb: #93a1a1;
-$solarized-light-sc: #2aa198;
-$solarized-light-sd: #586e75;
-$solarized-light-s2: #2aa198;
-$solarized-light-se: #cb4b16;
-$solarized-light-sh: #586e75;
-$solarized-light-si: #2aa198;
-$solarized-light-sx: #2aa198;
-$solarized-light-sr: #dc322f;
-$solarized-light-s1: #2aa198;
-$solarized-light-ss: #2aa198;
-$solarized-light-bp: #268bd2;
-$solarized-light-vc: #268bd2;
-$solarized-light-vg: #268bd2;
-$solarized-light-vi: #268bd2;
-$solarized-light-il: #2aa198;
-
-@mixin matchLine {
- color: $black-transparent;
- background: $solarized-light-matchline-bg;
-}
-
-.code.solarized-light {
- // Line numbers
- .line-numbers,
- .diff-line-num {
- background-color: $solarized-light-line-bg;
- }
-
- .diff-line-num,
- .diff-line-num a {
- color: $solarized-light-line-color;
- }
-
- // Code itself
- pre.code,
- .diff-line-num {
- border-color: $solarized-light-border;
- }
-
- &,
- pre.code,
- .line_holder .line_content {
- background-color: $solarized-light-pre-bg;
- color: $solarized-light-pre-color;
- }
-
- // Diff line
- .line_holder {
- &.match .line_content,
- &.old-nonewline .line_content,
- &.new-nonewline .line_content {
- @include matchLine;
- }
-
- td.diff-line-num.hll:not(.empty-cell),
- td.line_content.hll:not(.empty-cell) {
- background-color: $solarized-light-hll-bg;
- border-color: darken($solarized-light-hll-bg, 15%);
- }
-
- .diff-line-num.new,
- .line_content.new {
- @include diff_background($solarized-light-new-bg,
- $solarized-light-new-idiff, $solarized-light-border);
-
- &::before,
- a {
- color: $solarized-light-line-color-new;
- }
- }
-
- .diff-line-num.old,
- .line_content.old {
- @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
-
- &::before,
- a {
- color: $solarized-light-line-color-old;
- }
- }
-
- .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
- pre .hll {
- background-color: $solarized-light-hll-bg !important;
- }
-
- // Search result highlight
- span.highlight_word {
- background-color: $solarized-light-highlight !important;
- }
-
- // Links to URLs, emails, or dependencies
- .line a {
- color: $solarized-light-kd;
- }
-
- /* Solarized Light
-
- For use with Jekyll and Pygments
-
- http://ethanschoonover.com/solarized
-
- SOLARIZED HEX ROLE
- --------- -------- ------------------------------------------
- base01 #586e75 body text / default code / primary content
- base1 #93a1a1 comments / secondary content
- base3 #fdf6e3 background
- orange #cb4b16 constants
- red #dc322f regex, special keywords
- blue #268bd2 reserved keywords
- cyan #2aa198 strings, numbers
- green #859900 operators, other keywords
- */
-
- .c { color: $solarized-light-c; } /* Comment */
- .err { color: $solarized-light-err; } /* Error */
- .g { color: $solarized-light-g; } /* Generic */
- .k { color: $solarized-light-k; } /* Keyword */
- .l { color: $solarized-light-l; } /* Literal */
- .n { color: $solarized-light-n; } /* Name */
- .o { color: $solarized-light-o; } /* Operator */
- .x { color: $solarized-light-x; } /* Other */
- .p { color: $solarized-light-p; } /* Punctuation */
- .cm { color: $solarized-light-cm; } /* Comment.Multiline */
- .cp { color: $solarized-light-cp; } /* Comment.Preproc */
- .c1 { color: $solarized-light-c1; } /* Comment.Single */
- .cs { color: $solarized-light-cs; } /* Comment.Special */
- .gd { color: $solarized-light-gd; } /* Generic.Deleted */
- .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */
- .gr { color: $solarized-light-gr; } /* Generic.Error */
- .gh { color: $solarized-light-gh; } /* Generic.Heading */
- .gi { color: $solarized-light-gi; } /* Generic.Inserted */
- .go { color: $solarized-light-go; } /* Generic.Output */
- .gp { color: $solarized-light-gp; } /* Generic.Prompt */
- .gs { color: $solarized-light-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
- .gu { color: $solarized-light-gu; } /* Generic.Subheading */
- .gt { color: $solarized-light-gt; } /* Generic.Traceback */
- .kc { color: $solarized-light-kc; } /* Keyword.Constant */
- .kd { color: $solarized-light-kd; } /* Keyword.Declaration */
- .kn { color: $solarized-light-kn; } /* Keyword.Namespace */
- .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */
- .kr { color: $solarized-light-kr; } /* Keyword.Reserved */
- .kt { color: $solarized-light-kt; } /* Keyword.Type */
- .ld { color: $solarized-light-ld; } /* Literal.Date */
- .m { color: $solarized-light-m; } /* Literal.Number */
- .s { color: $solarized-light-s; } /* Literal.String */
- .na { color: $solarized-light-na; } /* Name.Attribute */
- .nb { color: $solarized-light-nb; } /* Name.Builtin */
- .nc { color: $solarized-light-nc; } /* Name.Class */
- .no { color: $solarized-light-no; } /* Name.Constant */
- .nd { color: $solarized-light-nd; } /* Name.Decorator */
- .ni { color: $solarized-light-ni; } /* Name.Entity */
- .ne { color: $solarized-light-ne; } /* Name.Exception */
- .nf { color: $solarized-light-nf; } /* Name.Function */
- .nl { color: $solarized-light-nl; } /* Name.Label */
- .nn { color: $solarized-light-nn; } /* Name.Namespace */
- .nx { color: $solarized-light-nx; } /* Name.Other */
- .py { color: $solarized-light-py; } /* Name.Property */
- .nt { color: $solarized-light-nt; } /* Name.Tag */
- .nv { color: $solarized-light-nv; } /* Name.Variable */
- .ow { color: $solarized-light-ow; } /* Operator.Word */
- .w { color: $solarized-light-w; } /* Text.Whitespace */
- .mf { color: $solarized-light-mf; } /* Literal.Number.Float */
- .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */
- .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */
- .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */
- .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */
- .sc { color: $solarized-light-sc; } /* Literal.String.Char */
- .sd { color: $solarized-light-sd; } /* Literal.String.Doc */
- .s2 { color: $solarized-light-s2; } /* Literal.String.Double */
- .se { color: $solarized-light-se; } /* Literal.String.Escape */
- .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */
- .si { color: $solarized-light-si; } /* Literal.String.Interpol */
- .sx { color: $solarized-light-sx; } /* Literal.String.Other */
- .sr { color: $solarized-light-sr; } /* Literal.String.Regex */
- .s1 { color: $solarized-light-s1; } /* Literal.String.Single */
- .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */
- .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */
- .vc { color: $solarized-light-vc; } /* Name.Variable.Class */
- .vg { color: $solarized-light-vg; } /* Name.Variable.Global */
- .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */
- .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */
-}
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
new file mode 100644
index 00000000000..16893dd047e
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -0,0 +1,261 @@
+/* https://github.com/MozMorris/tomorrow-pygments */
+
+@import "../common";
+
+/*
+* Dark syntax colors
+*/
+$dark-new-bg: rgba(51, 255, 51, 0.1);
+$dark-new-idiff: rgba(51, 255, 51, 0.2);
+$dark-old-bg: rgba(255, 51, 51, 0.2);
+$dark-old-idiff: rgba(255, 51, 51, 0.25);
+$dark-border: #808080;
+$dark-code-border: #666;
+$dark-main-bg: #1d1f21;
+$dark-main-color: #1d1f21;
+$dark-line-color: #c5c8c6;
+$dark-line-num-color: rgba(255, 255, 255, 0.3);
+$dark-line-num-color-new: #627165;
+$dark-line-num-color-old: #806565;
+$dark-diff-not-empty-bg: #557;
+$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;
+$dark-l: #de935f;
+$dark-n: #c5c8c6;
+$dark-o: #8abeb7;
+$dark-p: #c5c8c6;
+$dark-cm: #969896;
+$dark-cp: #969896;
+$dark-c1: #969896;
+$dark-cs: #969896;
+$dark-gd: #c66;
+$dark-gh: #c5c8c6;
+$dark-gi: #b5bd68;
+$dark-gp: #969896;
+$dark-gu: #8abeb7;
+$dark-kc: #b294bb;
+$dark-kd: #b294bb;
+$dark-kn: #8abeb7;
+$dark-kp: #b294bb;
+$dark-kr: #b294bb;
+$dark-kt: #f0c674;
+$dark-ld: #b5bd68;
+$dark-m: #de935f;
+$dark-s: #b5bd68;
+$dark-na: #81a2be;
+$dark-nb: #c5c8c6;
+$dark-nc: #f0c674;
+$dark-no: #c66;
+$dark-nd: #8abeb7;
+$dark-ni: #c5c8c6;
+$dark-ne: #c66;
+$dark-nf: #81a2be;
+$dark-nl: #c5c8c6;
+$dark-nn: #f0c674;
+$dark-nx: #81a2be;
+$dark-py: #c5c8c6;
+$dark-nt: #8abeb7;
+$dark-nv: #c66;
+$dark-ow: #8abeb7;
+$dark-w: #c5c8c6;
+$dark-mf: #de935f;
+$dark-mh: #de935f;
+$dark-mi: #de935f;
+$dark-mo: #de935f;
+$dark-sb: #b5bd68;
+$dark-sc: #c5c8c6;
+$dark-sd: #969896;
+$dark-s2: #b5bd68;
+$dark-se: #de935f;
+$dark-sh: #b5bd68;
+$dark-si: #de935f;
+$dark-sx: #b5bd68;
+$dark-sr: #b5bd68;
+$dark-s1: #b5bd68;
+$dark-ss: #b5bd68;
+$dark-bp: #c5c8c6;
+$dark-vc: #c66;
+$dark-vg: #c66;
+$dark-vi: #c66;
+$dark-il: #de935f;
+
+.code.dark {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $dark-main-bg;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $dark-line-num-color;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $dark-code-border;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $dark-main-bg;
+ color: $dark-line-color;
+ }
+
+ // Diff line
+ .line_holder {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
+ @include dark-diff-match-line;
+ }
+
+ td.diff-line-num.hll:not(.empty-cell),
+ td.line_content.hll:not(.empty-cell) {
+ background-color: $dark-diff-not-empty-bg;
+ border-color: darken($dark-diff-not-empty-bg, 15%);
+ }
+
+ .diff-line-num.new,
+ .line_content.new {
+ @include diff-background($dark-new-bg, $dark-new-idiff, $dark-border);
+
+ &::before,
+ a {
+ color: $dark-line-num-color-new;
+ }
+ }
+
+ .diff-line-num.old,
+ .line_content.old {
+ @include diff-background($dark-old-bg, $dark-old-idiff, $dark-border);
+
+ &::before,
+ a {
+ color: $dark-line-num-color-old;
+ }
+ }
+
+ .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
+ pre .hll {
+ background-color: $dark-pre-hll-bg !important;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $dark-highlight-bg !important;
+ color: $dark-highlight-color !important;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $dark-na;
+ }
+
+ .hll { background-color: $dark-hll-bg; }
+ .c { color: $dark-c; } /* Comment */
+ .err { color: $dark-err; } /* Error */
+ .k { color: $dark-k; } /* Keyword */
+ .l { color: $dark-l; } /* Literal */
+ .n { color: $dark-n; } /* Name */
+ .o { color: $dark-o; } /* Operator */
+ .p { color: $dark-p; } /* Punctuation */
+ .cm { color: $dark-cm; } /* Comment.Multiline */
+ .cp { color: $dark-cp; } /* Comment.Preproc */
+ .c1 { color: $dark-c1; } /* Comment.Single */
+ .cs { color: $dark-cs; } /* Comment.Special */
+ .gd { color: $dark-gd; } /* Generic.Deleted */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gh { color: $dark-gh; font-weight: $gl-font-weight-bold; } /* Generic.Heading */
+ .gi { color: $dark-gi; } /* Generic.Inserted */
+ .gp { color: $dark-gp; font-weight: $gl-font-weight-bold; } /* Generic.Prompt */
+ .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
+ .gu { color: $dark-gu; font-weight: $gl-font-weight-bold; } /* Generic.Subheading */
+ .kc { color: $dark-kc; } /* Keyword.Constant */
+ .kd { color: $dark-kd; } /* Keyword.Declaration */
+ .kn { color: $dark-kn; } /* Keyword.Namespace */
+ .kp { color: $dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $dark-kr; } /* Keyword.Reserved */
+ .kt { color: $dark-kt; } /* Keyword.Type */
+ .ld { color: $dark-ld; } /* Literal.Date */
+ .m { color: $dark-m; } /* Literal.Number */
+ .s { color: $dark-s; } /* Literal.String */
+ .na { color: $dark-na; } /* Name.Attribute */
+ .nb { color: $dark-nb; } /* Name.Builtin */
+ .nc { color: $dark-nc; } /* Name.Class */
+ .no { color: $dark-no; } /* Name.Constant */
+ .nd { color: $dark-nd; } /* Name.Decorator */
+ .ni { color: $dark-ni; } /* Name.Entity */
+ .ne { color: $dark-ne; } /* Name.Exception */
+ .nf { color: $dark-nf; } /* Name.Function */
+ .nl { color: $dark-nl; } /* Name.Label */
+ .nn { color: $dark-nn; } /* Name.Namespace */
+ .nx { color: $dark-nx; } /* Name.Other */
+ .py { color: $dark-py; } /* Name.Property */
+ .nt { color: $dark-nt; } /* Name.Tag */
+ .nv { color: $dark-nv; } /* Name.Variable */
+ .ow { color: $dark-ow; } /* Operator.Word */
+ .w { color: $dark-w; } /* Text.Whitespace */
+ .mf { color: $dark-mf; } /* Literal.Number.Float */
+ .mh { color: $dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $dark-sc; } /* Literal.String.Char */
+ .sd { color: $dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $dark-s2; } /* Literal.String.Double */
+ .se { color: $dark-se; } /* Literal.String.Escape */
+ .sh { color: $dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $dark-si; } /* Literal.String.Interpol */
+ .sx { color: $dark-sx; } /* Literal.String.Other */
+ .sr { color: $dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $dark-s1; } /* Literal.String.Single */
+ .ss { color: $dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $dark-vc; } /* Name.Variable.Class */
+ .vg { color: $dark-vg; } /* Name.Variable.Global */
+ .vi { color: $dark-vi; } /* Name.Variable.Instance */
+ .il { color: $dark-il; } /* Literal.Number.Integer.Long */
+}
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
new file mode 100644
index 00000000000..37fe61b925c
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -0,0 +1,259 @@
+/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
+
+@import "../common";
+
+/*
+* Monokai Colors
+*/
+$monokai-bg: #272822;
+$monokai-border: #555;
+$monokai-text-color: #f8f8f2;
+$monokai-line-num-color: rgba(255, 255, 255, 0.3);
+$monokai-line-num-color-new: #707565;
+$monokai-line-num-color-old: #7e736f;
+$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);
+
+$monokai-old-bg: rgba(254, 147, 140, 0.15);
+$monokai-old-idiff: rgba(254, 147, 140, 0.2);
+
+$monokai-hll: #49483e;
+$monokai-c: #75715e;
+$monokai-err-color: #960050;
+$monokai-err-bg: #1e0010;
+$monokai-k: #66d9ef;
+$monokai-l: #ae81ff;
+$monokai-n: #f8f8f2;
+$monokai-o: #f92672;
+$monokai-p: #f8f8f2;
+$monokai-cm: #75715e;
+$monokai-cp: #75715e;
+$monokai-c1: #75715e;
+$monokai-cs: #75715e;
+$monokai-kc: #66d9ef;
+$monokai-kd: #66d9ef;
+$monokai-kn: #f92672;
+$monokai-kp: #66d9ef;
+$monokai-kr: #66d9ef;
+$monokai-kt: #66d9ef;
+$monokai-ld: #e6db74;
+$monokai-m: #ae81ff;
+$monokai-s: #e6db74;
+$monokai-na: #a6e22e;
+$monokai-nb: #f8f8f2;
+$monokai-nc: #a6e22e;
+$monokai-no: #66d9ef;
+$monokai-nd: #a6e22e;
+$monokai-ni: #f8f8f2;
+$monokai-ne: #a6e22e;
+$monokai-nf: #a6e22e;
+$monokai-nl: #f8f8f2;
+$monokai-nn: #f8f8f2;
+$monokai-nx: #a6e22e;
+$monokai-py: #f8f8f2;
+$monokai-nt: #f92672;
+$monokai-nv: #f8f8f2;
+$monokai-ow: #f92672;
+$monokai-w: #f8f8f2;
+$monokai-mf: #ae81ff;
+$monokai-mh: #ae81ff;
+$monokai-mi: #ae81ff;
+$monokai-mo: #ae81ff;
+$monokai-sb: #e6db74;
+$monokai-sc: #e6db74;
+$monokai-sd: #e6db74;
+$monokai-s2: #e6db74;
+$monokai-se: #ae81ff;
+$monokai-sh: #e6db74;
+$monokai-si: #e6db74;
+$monokai-sx: #e6db74;
+$monokai-sr: #e6db74;
+$monokai-s1: #e6db74;
+$monokai-ss: #e6db74;
+$monokai-bp: #f8f8f2;
+$monokai-vc: #f8f8f2;
+$monokai-vg: #f8f8f2;
+$monokai-vi: #f8f8f2;
+$monokai-il: #ae81ff;
+$monokai-gu: #75715e;
+$monokai-gd: #f92672;
+$monokai-gi: #a6e22e;
+
+.code.monokai {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $monokai-bg;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $monokai-line-num-color;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $monokai-border;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $monokai-bg;
+ color: $monokai-text-color;
+ }
+
+ // Diff line
+ .line_holder {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
+ @include dark-diff-match-line;
+ }
+
+ td.diff-line-num.hll:not(.empty-cell),
+ td.line_content.hll:not(.empty-cell) {
+ background-color: $monokai-line-empty-bg;
+ border-color: $monokai-line-empty-border;
+ }
+
+ .diff-line-num.new,
+ .line_content.new {
+ @include diff-background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
+
+ &::before,
+ a {
+ color: $monokai-line-num-color-new;
+ }
+ }
+
+ .diff-line-num.old,
+ .line_content.old {
+ @include diff-background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
+
+ &::before,
+ a {
+ color: $monokai-line-num-color-old;
+ }
+ }
+
+ .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
+ pre .hll {
+ background-color: $monokai-hll !important;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $monokai-highlight-bg !important;
+ color: $black !important;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $monokai-k;
+ }
+
+ .hll { background-color: $monokai-hll; }
+ .c { color: $monokai-c; } /* Comment */
+ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */
+ .k { color: $monokai-k; } /* Keyword */
+ .l { color: $monokai-l; } /* Literal */
+ .n { color: $monokai-n; } /* Name */
+ .o { color: $monokai-o; } /* Operator */
+ .p { color: $monokai-p; } /* Punctuation */
+ .cm { color: $monokai-cm; } /* Comment.Multiline */
+ .cp { color: $monokai-cp; } /* Comment.Preproc */
+ .c1 { color: $monokai-c1; } /* Comment.Single */
+ .cs { color: $monokai-cs; } /* Comment.Special */
+ .ge { font-style: italic; } /* Generic.Emph */
+ .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
+ .kc { color: $monokai-kc; } /* Keyword.Constant */
+ .kd { color: $monokai-kd; } /* Keyword.Declaration */
+ .kn { color: $monokai-kn; } /* Keyword.Namespace */
+ .kp { color: $monokai-kp; } /* Keyword.Pseudo */
+ .kr { color: $monokai-kr; } /* Keyword.Reserved */
+ .kt { color: $monokai-kt; } /* Keyword.Type */
+ .ld { color: $monokai-ld; } /* Literal.Date */
+ .m { color: $monokai-m; } /* Literal.Number */
+ .s { color: $monokai-s; } /* Literal.String */
+ .na { color: $monokai-na; } /* Name.Attribute */
+ .nb { color: $monokai-nb; } /* Name.Builtin */
+ .nc { color: $monokai-nc; } /* Name.Class */
+ .no { color: $monokai-no; } /* Name.Constant */
+ .nd { color: $monokai-nd; } /* Name.Decorator */
+ .ni { color: $monokai-ni; } /* Name.Entity */
+ .ne { color: $monokai-ne; } /* Name.Exception */
+ .nf { color: $monokai-nf; } /* Name.Function */
+ .nl { color: $monokai-nl; } /* Name.Label */
+ .nn { color: $monokai-nn; } /* Name.Namespace */
+ .nx { color: $monokai-nx; } /* Name.Other */
+ .py { color: $monokai-py; } /* Name.Property */
+ .nt { color: $monokai-nt; } /* Name.Tag */
+ .nv { color: $monokai-nv; } /* Name.Variable */
+ .ow { color: $monokai-ow; } /* Operator.Word */
+ .w { color: $monokai-w; } /* Text.Whitespace */
+ .mf { color: $monokai-mf; } /* Literal.Number.Float */
+ .mh { color: $monokai-mh; } /* Literal.Number.Hex */
+ .mi { color: $monokai-mi; } /* Literal.Number.Integer */
+ .mo { color: $monokai-mo; } /* Literal.Number.Oct */
+ .sb { color: $monokai-sb; } /* Literal.String.Backtick */
+ .sc { color: $monokai-sc; } /* Literal.String.Char */
+ .sd { color: $monokai-sd; } /* Literal.String.Doc */
+ .s2 { color: $monokai-s2; } /* Literal.String.Double */
+ .se { color: $monokai-se; } /* Literal.String.Escape */
+ .sh { color: $monokai-sh; } /* Literal.String.Heredoc */
+ .si { color: $monokai-si; } /* Literal.String.Interpol */
+ .sx { color: $monokai-sx; } /* Literal.String.Other */
+ .sr { color: $monokai-sr; } /* Literal.String.Regex */
+ .s1 { color: $monokai-s1; } /* Literal.String.Single */
+ .ss { color: $monokai-ss; } /* Literal.String.Symbol */
+ .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $monokai-vc; } /* Name.Variable.Class */
+ .vg { color: $monokai-vg; } /* Name.Variable.Global */
+ .vi { color: $monokai-vi; } /* Name.Variable.Instance */
+ .il { color: $monokai-il; } /* Literal.Number.Integer.Long */
+ .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */
+ .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */
+ .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */
+}
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
new file mode 100644
index 00000000000..b4217aac37a
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -0,0 +1,238 @@
+/*
+* None Syntax Colors
+*/
+
+@import "../common";
+
+@mixin match-line {
+ color: $black-transparent;
+ background-color: $white-normal;
+}
+
+.code.none {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $gray-light;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $black-transparent;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $white-normal;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $white-light;
+ color: $gl-text-color;
+ }
+
+// Diff line
+
+ $none-over-bg: #ded7fc;
+ $none-expanded-border: #e0e0e0;
+ $none-expanded-bg: #e0e0e0;
+
+ .line_holder {
+
+ &.match .line_content,
+ .new-nonewline.line_content,
+ .old-nonewline.line_content {
+ @include match-line;
+ }
+
+ .diff-line-num {
+ &.old {
+ a {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.new {
+ a {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+ }
+
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $none-over-bg;
+ border-color: darken($none-over-bg, 5%);
+
+ a {
+ color: darken($none-over-bg, 15%);
+ }
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $white-light;
+ border-color: $white-normal;
+ }
+ }
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $none-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $none-expanded-bg;
+ border-color: $none-expanded-bg;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background-color: $white-normal;
+
+ &::before {
+ color: $gl-text-color;
+ }
+
+ span.idiff {
+ background-color: $white-normal;
+ text-decoration: underline;
+ }
+ }
+
+ &.new {
+ background-color: $white-normal;
+
+ &::before {
+ color: $gl-text-color;
+ }
+
+ span.idiff {
+ background-color: $white-normal;
+ text-decoration: underline;
+ }
+ }
+
+ &.match {
+ @include match-line;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $white-normal;
+ }
+ }
+ }
+
+ // highlight line via anchor
+ pre .hll {
+ background-color: $white-normal;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $white-normal;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $gl-text-color;
+ text-decoration: underline;
+ }
+
+ .hll { background-color: $white-light; }
+
+ .gd {
+ color: $gl-text-color;
+ background-color: $white-light;
+
+ .x {
+ color: $gl-text-color;
+ background-color: $white-light;
+ }
+ }
+
+ .gi {
+ color: $gl-text-color;
+ background-color: $white-light;
+
+ .x {
+ color: $gl-text-color;
+ background-color: $white-light;
+ }
+ }
+
+ .c { color: $gl-text-color; } /* Comment */
+ .err { color: $gl-text-color; } /* Error */
+ .g { color: $gl-text-color; } /* Generic */
+ .k { color: $gl-text-color; } /* Keyword */
+ .l { color: $gl-text-color; } /* Literal */
+ .n { color: $gl-text-color; } /* Name */
+ .o { color: $gl-text-color; } /* Operator */
+ .x { color: $gl-text-color; } /* Other */
+ .p { color: $gl-text-color; } /* Punctuation */
+ .cm { color: $gl-text-color; } /* Comment.Multiline */
+ .cp { color: $gl-text-color; } /* Comment.Preproc */
+ .c1 { color: $gl-text-color; } /* Comment.Single */
+ .cs { color: $gl-text-color; } /* Comment.Special */
+ .ge { color: $gl-text-color; } /* Generic.Emph */
+ .gr { color: $gl-text-color; } /* Generic.Error */
+ .gh { color: $gl-text-color; } /* Generic.Heading */
+ .go { color: $gl-text-color; } /* Generic.Output */
+ .gp { color: $gl-text-color; } /* Generic.Prompt */
+ .gs { color: $gl-text-color; } /* Generic.Strong */
+ .gu { color: $gl-text-color; } /* Generic.Subheading */
+ .gt { color: $gl-text-color; } /* Generic.Traceback */
+ .kc { color: $gl-text-color; } /* Keyword.Constant */
+ .kd { color: $gl-text-color; } /* Keyword.Declaration */
+ .kn { color: $gl-text-color; } /* Keyword.Namespace */
+ .kp { color: $gl-text-color; } /* Keyword.Pseudo */
+ .kr { color: $gl-text-color; } /* Keyword.Reserved */
+ .kt { color: $gl-text-color; } /* Keyword.Type */
+ .ld { color: $gl-text-color; } /* Literal.Date */
+ .m { color: $gl-text-color; } /* Literal.Number */
+ .s { color: $gl-text-color; } /* Literal.String */
+ .na { color: $gl-text-color; } /* Name.Attribute */
+ .nb { color: $gl-text-color; } /* Name.Builtin */
+ .nc { color: $gl-text-color; } /* Name.Class */
+ .no { color: $gl-text-color; } /* Name.Constant */
+ .nd { color: $gl-text-color; } /* Name.Decorator */
+ .ni { color: $gl-text-color; } /* Name.Entity */
+ .ne { color: $gl-text-color; } /* Name.Exception */
+ .nf { color: $gl-text-color; } /* Name.Function */
+ .nl { color: $gl-text-color; } /* Name.Label */
+ .nn { color: $gl-text-color; } /* Name.Namespace */
+ .nx { color: $gl-text-color; } /* Name.Other */
+ .py { color: $gl-text-color; } /* Name.Property */
+ .nt { color: $gl-text-color; } /* Name.Tag */
+ .nv { color: $gl-text-color; } /* Name.Variable */
+ .ow { color: $gl-text-color; } /* Operator.Word */
+ .w { color: $gl-text-color; } /* Text.Whitespace */
+ .mf { color: $gl-text-color; } /* Literal.Number.Float */
+ .mh { color: $gl-text-color; } /* Literal.Number.Hex */
+ .mi { color: $gl-text-color; } /* Literal.Number.Integer */
+ .mo { color: $gl-text-color; } /* Literal.Number.Oct */
+ .sb { color: $gl-text-color; } /* Literal.String.Backtick */
+ .sc { color: $gl-text-color; } /* Literal.String.Char */
+ .sd { color: $gl-text-color; } /* Literal.String.Doc */
+ .s2 { color: $gl-text-color; } /* Literal.String.Double */
+ .se { color: $gl-text-color; } /* Literal.String.Escape */
+ .sh { color: $gl-text-color; } /* Literal.String.Heredoc */
+ .si { color: $gl-text-color; } /* Literal.String.Interpol */
+ .sx { color: $gl-text-color; } /* Literal.String.Other */
+ .sr { color: $gl-text-color; } /* Literal.String.Regex */
+ .s1 { color: $gl-text-color; } /* Literal.String.Single */
+ .ss { color: $gl-text-color; } /* Literal.String.Symbol */
+ .bp { color: $gl-text-color; } /* Name.Builtin.Pseudo */
+ .vc { color: $gl-text-color; } /* Name.Variable.Class */
+ .vg { color: $gl-text-color; } /* Name.Variable.Global */
+ .vi { color: $gl-text-color; } /* Name.Variable.Instance */
+ .il { color: $gl-text-color; } /* Literal.Number.Integer.Long */
+
+}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
new file mode 100644
index 00000000000..a4e9eda22c9
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -0,0 +1,286 @@
+/* https://gist.github.com/qguv/7936275 */
+
+@import "../common";
+
+/*
+* Solarized dark colors
+*/
+$solarized-dark-new-bg: rgba(133, 153, 0, 0.15);
+$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-dark-old-bg: rgba(220, 50, 47, 0.3);
+$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-dark-border: #113b46;
+$solarized-dark-pre-bg: #002b36;
+$solarized-dark-pre-color: #93a1a1;
+$solarized-dark-pre-border: #113b46;
+$solarized-dark-line-bg: #002b36;
+$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
+$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;
+$solarized-dark-k: #859900;
+$solarized-dark-l: #93a1a1;
+$solarized-dark-n: #93a1a1;
+$solarized-dark-o: #859900;
+$solarized-dark-x: #cb4b16;
+$solarized-dark-p: #93a1a1;
+$solarized-dark-cm: #586e75;
+$solarized-dark-cp: #859900;
+$solarized-dark-c1: #586e75;
+$solarized-dark-cs: #859900;
+$solarized-dark-gd: #2aa198;
+$solarized-dark-ge: #93a1a1;
+$solarized-dark-gr: #dc322f;
+$solarized-dark-gh: #cb4b16;
+$solarized-dark-gi: #859900;
+$solarized-dark-go: #93a1a1;
+$solarized-dark-gp: #93a1a1;
+$solarized-dark-gs: #93a1a1;
+$solarized-dark-gu: #cb4b16;
+$solarized-dark-gt: #93a1a1;
+$solarized-dark-kc: #cb4b16;
+$solarized-dark-kd: #268bd2;
+$solarized-dark-kn: #859900;
+$solarized-dark-kp: #859900;
+$solarized-dark-kr: #268bd2;
+$solarized-dark-kt: #dc322f;
+$solarized-dark-ld: #93a1a1;
+$solarized-dark-m: #2aa198;
+$solarized-dark-s: #2aa198;
+$solarized-dark-na: #93a1a1;
+$solarized-dark-nb: #b58900;
+$solarized-dark-nc: #268bd2;
+$solarized-dark-no: #cb4b16;
+$solarized-dark-nd: #268bd2;
+$solarized-dark-ni: #cb4b16;
+$solarized-dark-ne: #cb4b16;
+$solarized-dark-nf: #268bd2;
+$solarized-dark-nl: #93a1a1;
+$solarized-dark-nn: #93a1a1;
+$solarized-dark-nx: #93a1a1;
+$solarized-dark-py: #93a1a1;
+$solarized-dark-nt: #268bd2;
+$solarized-dark-nv: #268bd2;
+$solarized-dark-ow: #859900;
+$solarized-dark-w: #93a1a1;
+$solarized-dark-mf: #2aa198;
+$solarized-dark-mh: #2aa198;
+$solarized-dark-mi: #2aa198;
+$solarized-dark-mo: #2aa198;
+$solarized-dark-sb: #586e75;
+$solarized-dark-sc: #2aa198;
+$solarized-dark-sd: #93a1a1;
+$solarized-dark-s2: #2aa198;
+$solarized-dark-se: #cb4b16;
+$solarized-dark-sh: #93a1a1;
+$solarized-dark-si: #2aa198;
+$solarized-dark-sx: #2aa198;
+$solarized-dark-sr: #dc322f;
+$solarized-dark-s1: #2aa198;
+$solarized-dark-ss: #2aa198;
+$solarized-dark-bp: #268bd2;
+$solarized-dark-vc: #268bd2;
+$solarized-dark-vg: #268bd2;
+$solarized-dark-vi: #268bd2;
+$solarized-dark-il: #2aa198;
+
+.code.solarized-dark {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $solarized-dark-line-bg;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $solarized-dark-line-color;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $solarized-dark-pre-border;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $solarized-dark-pre-bg;
+ color: $solarized-dark-pre-color;
+ }
+
+ // Diff line
+ .line_holder {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
+ @include dark-diff-match-line;
+ }
+
+ td.diff-line-num.hll:not(.empty-cell),
+ td.line_content.hll:not(.empty-cell) {
+ background-color: $solarized-dark-hll-bg;
+ border-color: darken($solarized-dark-hll-bg, 15%);
+ }
+
+ .diff-line-num.new,
+ .line_content.new {
+ @include diff-background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
+
+ &::before,
+ a {
+ color: $solarized-dark-line-color-new;
+ }
+ }
+
+ .diff-line-num.old,
+ .line_content.old {
+ @include diff-background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
+
+ &::before,
+ a {
+ color: $solarized-dark-line-color-old;
+ }
+ }
+
+ .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
+ pre .hll {
+ background-color: $solarized-dark-hll-bg !important;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $solarized-dark-highlight !important;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $solarized-dark-kd;
+ }
+
+ /* Solarized Dark
+
+ For use with Jekyll and Pygments
+
+ http://ethanschoonover.com/solarized
+
+ SOLARIZED HEX ROLE
+ --------- -------- ------------------------------------------
+ base03 #002b36 background
+ base01 #586e75 comments / secondary content
+ base1 #93a1a1 body text / default code / primary content
+ orange #cb4b16 constants
+ red #dc322f regex, special keywords
+ blue #268bd2 reserved keywords
+ cyan #2aa198 strings, numbers
+ green #859900 operators, other keywords
+ */
+
+ .c { color: $solarized-dark-c; } /* Comment */
+ .err { color: $solarized-dark-err; } /* Error */
+ .g { color: $solarized-dark-g; } /* Generic */
+ .k { color: $solarized-dark-k; } /* Keyword */
+ .l { color: $solarized-dark-l; } /* Literal */
+ .n { color: $solarized-dark-n; } /* Name */
+ .o { color: $solarized-dark-o; } /* Operator */
+ .x { color: $solarized-dark-x; } /* Other */
+ .p { color: $solarized-dark-p; } /* Punctuation */
+ .cm { color: $solarized-dark-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-dark-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-dark-c1; } /* Comment.Single */
+ .cs { color: $solarized-dark-cs; } /* Comment.Special */
+ .gd { color: $solarized-dark-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-dark-gr; } /* Generic.Error */
+ .gh { color: $solarized-dark-gh; } /* Generic.Heading */
+ .gi { color: $solarized-dark-gi; } /* Generic.Inserted */
+ .go { color: $solarized-dark-go; } /* Generic.Output */
+ .gp { color: $solarized-dark-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-dark-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
+ .gu { color: $solarized-dark-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-dark-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-dark-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-dark-kt; } /* Keyword.Type */
+ .ld { color: $solarized-dark-ld; } /* Literal.Date */
+ .m { color: $solarized-dark-m; } /* Literal.Number */
+ .s { color: $solarized-dark-s; } /* Literal.String */
+ .na { color: $solarized-dark-na; } /* Name.Attribute */
+ .nb { color: $solarized-dark-nb; } /* Name.Builtin */
+ .nc { color: $solarized-dark-nc; } /* Name.Class */
+ .no { color: $solarized-dark-no; } /* Name.Constant */
+ .nd { color: $solarized-dark-nd; } /* Name.Decorator */
+ .ni { color: $solarized-dark-ni; } /* Name.Entity */
+ .ne { color: $solarized-dark-ne; } /* Name.Exception */
+ .nf { color: $solarized-dark-nf; } /* Name.Function */
+ .nl { color: $solarized-dark-nl; } /* Name.Label */
+ .nn { color: $solarized-dark-nn; } /* Name.Namespace */
+ .nx { color: $solarized-dark-nx; } /* Name.Other */
+ .py { color: $solarized-dark-py; } /* Name.Property */
+ .nt { color: $solarized-dark-nt; } /* Name.Tag */
+ .nv { color: $solarized-dark-nv; } /* Name.Variable */
+ .ow { color: $solarized-dark-ow; } /* Operator.Word */
+ .w { color: $solarized-dark-w; } /* Text.Whitespace */
+ .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-dark-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */
+ .se { color: $solarized-dark-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-dark-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-dark-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */
+}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
new file mode 100644
index 00000000000..b604d1ccb6c
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -0,0 +1,294 @@
+/* https://gist.github.com/qguv/7936275 */
+
+@import "../common";
+
+/*
+* Solarized light syntax colors
+*/
+$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4);
+$solarized-light-new-bg: rgba(133, 153, 0, 0.2);
+$solarized-light-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-light-old-bg: rgba(220, 50, 47, 0.2);
+$solarized-light-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-light-border: #c5d0d4;
+$solarized-light-pre-bg: #002b36;
+$solarized-light-pre-bg: #fdf6e3;
+$solarized-light-pre-color: #586e75;
+$solarized-light-line-bg: #fdf6e3;
+$solarized-light-line-color: rgba(0, 0, 0, 0.3);
+$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;
+$solarized-light-k: #859900;
+$solarized-light-l: #586e75;
+$solarized-light-n: #586e75;
+$solarized-light-o: #859900;
+$solarized-light-x: #cb4b16;
+$solarized-light-p: #586e75;
+$solarized-light-cm: #93a1a1;
+$solarized-light-cp: #859900;
+$solarized-light-c1: #93a1a1;
+$solarized-light-cs: #859900;
+$solarized-light-gd: #2aa198;
+$solarized-light-ge: #586e75;
+$solarized-light-gr: #dc322f;
+$solarized-light-gh: #cb4b16;
+$solarized-light-gi: #859900;
+$solarized-light-go: #586e75;
+$solarized-light-gp: #586e75;
+$solarized-light-gs: #586e75;
+$solarized-light-gu: #cb4b16;
+$solarized-light-gt: #586e75;
+$solarized-light-kc: #cb4b16;
+$solarized-light-kd: #268bd2;
+$solarized-light-kn: #859900;
+$solarized-light-kp: #859900;
+$solarized-light-kr: #268bd2;
+$solarized-light-kt: #dc322f;
+$solarized-light-ld: #586e75;
+$solarized-light-m: #2aa198;
+$solarized-light-s: #2aa198;
+$solarized-light-na: #586e75;
+$solarized-light-nb: #b58900;
+$solarized-light-nc: #268bd2;
+$solarized-light-no: #cb4b16;
+$solarized-light-nd: #268bd2;
+$solarized-light-ni: #cb4b16;
+$solarized-light-ne: #cb4b16;
+$solarized-light-nf: #268bd2;
+$solarized-light-nl: #586e75;
+$solarized-light-nn: #586e75;
+$solarized-light-nx: #586e75;
+$solarized-light-py: #586e75;
+$solarized-light-nt: #268bd2;
+$solarized-light-nv: #268bd2;
+$solarized-light-ow: #859900;
+$solarized-light-w: #586e75;
+$solarized-light-mf: #2aa198;
+$solarized-light-mh: #2aa198;
+$solarized-light-mi: #2aa198;
+$solarized-light-mo: #2aa198;
+$solarized-light-sb: #93a1a1;
+$solarized-light-sc: #2aa198;
+$solarized-light-sd: #586e75;
+$solarized-light-s2: #2aa198;
+$solarized-light-se: #cb4b16;
+$solarized-light-sh: #586e75;
+$solarized-light-si: #2aa198;
+$solarized-light-sx: #2aa198;
+$solarized-light-sr: #dc322f;
+$solarized-light-s1: #2aa198;
+$solarized-light-ss: #2aa198;
+$solarized-light-bp: #268bd2;
+$solarized-light-vc: #268bd2;
+$solarized-light-vg: #268bd2;
+$solarized-light-vi: #268bd2;
+$solarized-light-il: #2aa198;
+
+@mixin match-line {
+ color: $black-transparent;
+ background: $solarized-light-matchline-bg;
+}
+
+.code.solarized-light {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $solarized-light-line-bg;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $solarized-light-line-color;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $solarized-light-border;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $solarized-light-pre-bg;
+ color: $solarized-light-pre-color;
+ }
+
+ // Diff line
+ .line_holder {
+ &.match .line_content,
+ &.old-nonewline .line_content,
+ &.new-nonewline .line_content {
+ @include match-line;
+ }
+
+ td.diff-line-num.hll:not(.empty-cell),
+ td.line_content.hll:not(.empty-cell) {
+ background-color: $solarized-light-hll-bg;
+ border-color: darken($solarized-light-hll-bg, 15%);
+ }
+
+ .diff-line-num.new,
+ .line_content.new {
+ @include diff-background($solarized-light-new-bg,
+ $solarized-light-new-idiff, $solarized-light-border);
+
+ &::before,
+ a {
+ color: $solarized-light-line-color-new;
+ }
+ }
+
+ .diff-line-num.old,
+ .line_content.old {
+ @include diff-background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
+
+ &::before,
+ a {
+ color: $solarized-light-line-color-old;
+ }
+ }
+
+ .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 match-line;
+ }
+
+ &: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
+ pre .hll {
+ background-color: $solarized-light-hll-bg !important;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $solarized-light-highlight !important;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $solarized-light-kd;
+ }
+
+ /* Solarized Light
+
+ For use with Jekyll and Pygments
+
+ http://ethanschoonover.com/solarized
+
+ SOLARIZED HEX ROLE
+ --------- -------- ------------------------------------------
+ base01 #586e75 body text / default code / primary content
+ base1 #93a1a1 comments / secondary content
+ base3 #fdf6e3 background
+ orange #cb4b16 constants
+ red #dc322f regex, special keywords
+ blue #268bd2 reserved keywords
+ cyan #2aa198 strings, numbers
+ green #859900 operators, other keywords
+ */
+
+ .c { color: $solarized-light-c; } /* Comment */
+ .err { color: $solarized-light-err; } /* Error */
+ .g { color: $solarized-light-g; } /* Generic */
+ .k { color: $solarized-light-k; } /* Keyword */
+ .l { color: $solarized-light-l; } /* Literal */
+ .n { color: $solarized-light-n; } /* Name */
+ .o { color: $solarized-light-o; } /* Operator */
+ .x { color: $solarized-light-x; } /* Other */
+ .p { color: $solarized-light-p; } /* Punctuation */
+ .cm { color: $solarized-light-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-light-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-light-c1; } /* Comment.Single */
+ .cs { color: $solarized-light-cs; } /* Comment.Special */
+ .gd { color: $solarized-light-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-light-gr; } /* Generic.Error */
+ .gh { color: $solarized-light-gh; } /* Generic.Heading */
+ .gi { color: $solarized-light-gi; } /* Generic.Inserted */
+ .go { color: $solarized-light-go; } /* Generic.Output */
+ .gp { color: $solarized-light-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-light-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
+ .gu { color: $solarized-light-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-light-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-light-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-light-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-light-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-light-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-light-kt; } /* Keyword.Type */
+ .ld { color: $solarized-light-ld; } /* Literal.Date */
+ .m { color: $solarized-light-m; } /* Literal.Number */
+ .s { color: $solarized-light-s; } /* Literal.String */
+ .na { color: $solarized-light-na; } /* Name.Attribute */
+ .nb { color: $solarized-light-nb; } /* Name.Builtin */
+ .nc { color: $solarized-light-nc; } /* Name.Class */
+ .no { color: $solarized-light-no; } /* Name.Constant */
+ .nd { color: $solarized-light-nd; } /* Name.Decorator */
+ .ni { color: $solarized-light-ni; } /* Name.Entity */
+ .ne { color: $solarized-light-ne; } /* Name.Exception */
+ .nf { color: $solarized-light-nf; } /* Name.Function */
+ .nl { color: $solarized-light-nl; } /* Name.Label */
+ .nn { color: $solarized-light-nn; } /* Name.Namespace */
+ .nx { color: $solarized-light-nx; } /* Name.Other */
+ .py { color: $solarized-light-py; } /* Name.Property */
+ .nt { color: $solarized-light-nt; } /* Name.Tag */
+ .nv { color: $solarized-light-nv; } /* Name.Variable */
+ .ow { color: $solarized-light-ow; } /* Operator.Word */
+ .w { color: $solarized-light-w; } /* Text.Whitespace */
+ .mf { color: $solarized-light-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-light-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-light-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-light-s2; } /* Literal.String.Double */
+ .se { color: $solarized-light-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-light-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-light-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-light-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-light-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-light-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-light-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */
+}
diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss
new file mode 100644
index 00000000000..7239086f649
--- /dev/null
+++ b/app/assets/stylesheets/highlight/themes/white.scss
@@ -0,0 +1,3 @@
+.code.white {
+ @import "../white_base";
+}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
deleted file mode 100644
index 355c8d223f7..00000000000
--- a/app/assets/stylesheets/highlight/white.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.code.white {
- @import "white_base";
-}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 90a5250c247..ee0ec94c636 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -1,5 +1,7 @@
/* https://github.com/aahan/pygments-github-style */
+@import './common';
+
/*
* White Syntax Colors
*/
@@ -35,16 +37,16 @@ $white-kt: #458;
$white-m: #099;
$white-s: #d14;
$white-n: #333;
-$white-na: teal;
+$white-na: #008080;
$white-nb: #0086b3;
$white-nc: #458;
-$white-no: teal;
-$white-ni: purple;
+$white-no: #008080;
+$white-ni: #800080;
$white-ne: #900;
$white-nf: #900;
$white-nn: #555;
-$white-nt: navy;
-$white-nv: teal;
+$white-nt: #000080;
+$white-nv: #008080;
$white-w: #bbb;
$white-mf: #099;
$white-mh: #099;
@@ -62,20 +64,20 @@ $white-sr: #009926;
$white-s1: #d14;
$white-ss: #990073;
$white-bp: #999;
-$white-vc: teal;
-$white-vg: teal;
-$white-vi: teal;
+$white-vc: #008080;
+$white-vg: #008080;
+$white-vi: #008080;
$white-il: #099;
$white-gc-color: #999;
$white-gc-bg: #eaf2f5;
-@mixin matchLine {
+@mixin match-line {
color: $black-transparent;
background-color: $gray-light;
}
- // Line numbers
+// Line numbers
.line-numbers,
.diff-line-num {
background-color: $gray-light;
@@ -101,11 +103,10 @@ pre.code,
// Diff line
.line_holder {
-
&.match .line_content,
.new-nonewline.line_content,
.old-nonewline.line_content {
- @include matchLine;
+ @include match-line;
}
.diff-line-num {
@@ -185,7 +186,7 @@ pre.code,
}
&.match {
- @include matchLine;
+ @include match-line;
}
&.hll:not(.empty-cell) {
@@ -199,25 +200,38 @@ pre .hll {
background-color: $white-pre-hll-bg !important;
}
- // Search result highlight
+// Search result highlight
span.highlight_word {
background-color: $white-highlight !important;
}
- // Links to URLs, emails, or dependencies
+// Links to URLs, emails, or dependencies
.line a {
color: $white-nb;
}
.hll { background-color: $white-hll-bg; }
-.c { color: $white-c; font-style: italic; }
-.err { color: $white-err; background-color: $white-err-bg; }
+
+.c { color: $white-c;
+ font-style: italic; }
+
+.err { color: $white-err;
+ background-color: $white-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
-.cm { color: $white-cm; font-style: italic; }
-.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
-.c1 { color: $white-c1; font-style: italic; }
-.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
+
+.cm { color: $white-cm;
+ font-style: italic; }
+
+.cp { color: $white-cp;
+ font-weight: $gl-font-weight-bold; }
+
+.c1 { color: $white-c1;
+ font-style: italic; }
+
+.cs { color: $white-cs;
+ font-weight: $gl-font-weight-bold;
+ font-style: italic; }
.gd {
color: $white-gd;
@@ -246,24 +260,34 @@ span.highlight_word {
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
-.gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
+
+.gu { color: $white-gu;
+ font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
-.kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
+
+.kt { color: $white-kt;
+ font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
-.nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
+
+.nc { color: $white-nc;
+ font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
-.ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
-.nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
+
+.ne { color: $white-ne;
+ font-weight: $gl-font-weight-bold; }
+
+.nf { color: $white-nf;
+ font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
@@ -289,4 +313,6 @@ span.highlight_word {
.vg { color: $white-vg; }
.vi { color: $white-vi; }
.il { color: $white-il; }
-.gc { color: $white-gc-color; background-color: $white-gc-bg; }
+
+.gc { color: $white-gc-color;
+ background-color: $white-gc-bg; }
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 8b234a5a656..33c114838c2 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -1,4 +1,4 @@
-@import "framework/variables";
+@import 'framework/variables';
// This file is largely copied from `highlight/white.scss`, but modified to
// avoid all descendant selectors (`table td`). This is because the CSS inlining
@@ -40,16 +40,16 @@ $highlighted-kt: #458;
$highlighted-m: #099;
$highlighted-s: #d14;
$highlighted-n: #333;
-$highlighted-na: teal;
+$highlighted-na: #008080;
$highlighted-nb: #0086b3;
$highlighted-nc: #458;
-$highlighted-no: teal;
-$highlighted-ni: purple;
+$highlighted-no: #008080;
+$highlighted-ni: #800080;
$highlighted-ne: #900;
$highlighted-nf: #900;
$highlighted-nn: #555;
-$highlighted-nt: navy;
-$highlighted-nv: teal;
+$highlighted-nt: #000080;
+$highlighted-nv: #008080;
$highlighted-w: #bbb;
$highlighted-mf: #099;
$highlighted-mh: #099;
@@ -67,9 +67,9 @@ $highlighted-sr: #009926;
$highlighted-s1: #d14;
$highlighted-ss: #990073;
$highlighted-bp: #999;
-$highlighted-vc: teal;
-$highlighted-vg: teal;
-$highlighted-vi: teal;
+$highlighted-vc: #008080;
+$highlighted-vg: #008080;
+$highlighted-vi: #008080;
$highlighted-il: #099;
$highlighted-gc: #999;
$highlighted-gc-bg: #eaf2f5;
@@ -151,14 +151,27 @@ span.highlight_word {
}
.hll { background-color: $highlighted-hll-bg; }
-.c { color: $highlighted-c; font-style: italic; }
-.err { color: $highlighted-err; background-color: $highlighted-err-bg; }
+
+.c { color: $highlighted-c;
+ font-style: italic; }
+
+.err { color: $highlighted-err;
+ background-color: $highlighted-err-bg; }
.k { font-weight: $gl-font-weight-bold; }
.o { font-weight: $gl-font-weight-bold; }
-.cm { color: $highlighted-cm; font-style: italic; }
-.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
-.c1 { color: $highlighted-c1; font-style: italic; }
-.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
+
+.cm { color: $highlighted-cm;
+ font-style: italic; }
+
+.cp { color: $highlighted-cp;
+ font-weight: $gl-font-weight-bold; }
+
+.c1 { color: $highlighted-c1;
+ font-style: italic; }
+
+.cs { color: $highlighted-cs;
+ font-weight: $gl-font-weight-bold;
+ font-style: italic; }
.gd {
color: $highlighted-gd;
@@ -187,24 +200,34 @@ span.highlight_word {
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
.gs { font-weight: $gl-font-weight-bold; }
-.gu { color: $highlighted-gu; font-weight: $gl-font-weight-bold; }
+
+.gu { color: $highlighted-gu;
+ font-weight: $gl-font-weight-bold; }
.gt { color: $highlighted-gt; }
.kc { font-weight: $gl-font-weight-bold; }
.kd { font-weight: $gl-font-weight-bold; }
.kn { font-weight: $gl-font-weight-bold; }
.kp { font-weight: $gl-font-weight-bold; }
.kr { font-weight: $gl-font-weight-bold; }
-.kt { color: $highlighted-kt; font-weight: $gl-font-weight-bold; }
+
+.kt { color: $highlighted-kt;
+ font-weight: $gl-font-weight-bold; }
.m { color: $highlighted-m; }
.s { color: $highlighted-s; }
.n { color: $highlighted-n; }
.na { color: $highlighted-na; }
.nb { color: $highlighted-nb; }
-.nc { color: $highlighted-nc; font-weight: $gl-font-weight-bold; }
+
+.nc { color: $highlighted-nc;
+ font-weight: $gl-font-weight-bold; }
.no { color: $highlighted-no; }
.ni { color: $highlighted-ni; }
-.ne { color: $highlighted-ne; font-weight: $gl-font-weight-bold; }
-.nf { color: $highlighted-nf; font-weight: $gl-font-weight-bold; }
+
+.ne { color: $highlighted-ne;
+ font-weight: $gl-font-weight-bold; }
+
+.nf { color: $highlighted-nf;
+ font-weight: $gl-font-weight-bold; }
.nn { color: $highlighted-nn; }
.nt { color: $highlighted-nt; }
.nv { color: $highlighted-nv; }
@@ -230,4 +253,6 @@ span.highlight_word {
.vg { color: $highlighted-vg; }
.vi { color: $highlighted-vi; }
.il { color: $highlighted-il; }
-.gc { color: $highlighted-gc; background-color: $highlighted-gc-bg; }
+
+.gc { color: $highlighted-gc;
+ background-color: $highlighted-gc-bg; }
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 98d0a2d43ea..a80158943c6 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -130,7 +130,7 @@ $ide-commit-header-height: 48px;
background: none;
border: 0;
border-radius: $border-radius-default;
- color: $theme-gray-900;
+ color: $gray-900;
svg {
position: relative;
@@ -145,7 +145,7 @@ $ide-commit-header-height: 48px;
}
&:not([disabled]):hover {
- background-color: $theme-gray-200;
+ background-color: $gray-200;
}
&:not([disabled]):focus {
@@ -265,7 +265,7 @@ $ide-commit-header-height: 48px;
.margin {
background-color: $white-light;
- border-right: 1px solid $theme-gray-100;
+ border-right: 1px solid $gray-100;
.line-insert {
border-right: 1px solid $line-added-dark;
@@ -292,7 +292,7 @@ $ide-commit-header-height: 48px;
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
- background-color: $theme-gray-50;
+ background-color: $gray-50;
}
}
}
@@ -395,6 +395,11 @@ $ide-commit-header-height: 48px;
svg {
vertical-align: sub;
}
+
+ .ide-status-avatar {
+ float: none;
+ margin: 0 0 1px;
+ }
}
.ide-status-file {
@@ -527,7 +532,7 @@ $ide-commit-header-height: 48px;
display: block;
margin-left: auto;
margin-right: auto;
- color: $theme-gray-700;
+ color: $gray-700;
}
.file-status-icon {
@@ -551,7 +556,7 @@ $ide-commit-header-height: 48px;
&:hover,
&:focus {
- background: $theme-gray-100;
+ background: $gray-100;
outline: 0;
@@ -563,7 +568,7 @@ $ide-commit-header-height: 48px;
}
&:active {
- background: $theme-gray-200;
+ background: $gray-200;
}
&.is-active {
@@ -677,25 +682,6 @@ $ide-commit-header-height: 48px;
flex: 1;
}
-.drag-handle {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 4px;
-
- &:hover {
- background-color: $white-normal;
- }
-
- &.drag-right {
- right: 0;
- }
-
- &.drag-left {
- left: 0;
- }
-}
-
.ide-commit-list-container {
display: flex;
flex: 1;
@@ -767,12 +753,12 @@ $ide-commit-header-height: 48px;
&:hover {
color: $gl-text-color;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
}
&:focus {
color: $gl-text-color;
- background-color: $theme-gray-200;
+ background-color: $gray-200;
}
&.active {
@@ -811,26 +797,6 @@ $ide-commit-header-height: 48px;
z-index: 1;
}
-.ide-file-finder-overlay {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 100;
-}
-
-.ide-file-finder {
- top: 10px;
- left: 50%;
- transform: translateX(-50%);
-
- .highlighted {
- color: $blue-500;
- font-weight: $gl-font-weight-bold;
- }
-}
-
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
@@ -1273,10 +1239,10 @@ $ide-commit-header-height: 48px;
.ide-entry-dropdown-toggle {
padding: $gl-padding-4;
color: $gl-text-color;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
&:hover {
- background-color: $theme-gray-200;
+ background-color: $gray-200;
}
&:active,
@@ -1331,7 +1297,7 @@ $ide-commit-header-height: 48px;
&:focus {
outline: 0;
box-shadow: none;
- border-color: $theme-gray-200;
+ border-color: $gray-200;
}
}
@@ -1358,7 +1324,7 @@ $ide-commit-header-height: 48px;
.ide-commit-editor-header {
height: 65px;
padding: 8px 16px;
- background-color: $theme-gray-50;
+ background-color: $gray-50;
box-shadow: inset 0 -1px $white-dark;
}
@@ -1370,7 +1336,7 @@ $ide-commit-header-height: 48px;
.ide-file-icon-holder {
display: flex;
align-items: center;
- color: $theme-gray-700;
+ color: $gray-700;
}
.file-row:hover,
diff --git a/app/assets/stylesheets/page_bundles/xterm.scss b/app/assets/stylesheets/page_bundles/xterm.scss
index 7f040ac9b96..de3f2a1177d 100644
--- a/app/assets/stylesheets/page_bundles/xterm.scss
+++ b/app/assets/stylesheets/page_bundles/xterm.scss
@@ -6,11 +6,11 @@
$black: #000;
$red: #ea1010;
- $green: #009900;
- $yellow: #999900;
+ $green: #090;
+ $yellow: #990;
$blue: #0073e6;
$magenta: #d411d4;
- $cyan: #009999;
+ $cyan: #099;
$white: #ccc;
$l-black: #373b41;
$l-red: #ff6161;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 37984a8666f..81216b2b98e 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -3,7 +3,6 @@
}
.user-can-drag {
- cursor: -webkit-grab;
cursor: grab;
}
@@ -12,12 +11,8 @@
opacity: 1 !important;
* {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
user-select: none;
// !important to make sure no style can override this when dragging
- cursor: -webkit-grabbing !important;
cursor: grabbing !important;
}
}
@@ -26,13 +21,15 @@
opacity: 0.3;
}
+.dropdown-projects {
+ .dropdown-content {
+ max-height: 200px;
+ }
+}
+
.dropdown-menu-issues-board-new {
width: 320px;
- .open & {
- max-height: 400px;
- }
-
.dropdown-content {
max-height: 162px;
}
@@ -162,6 +159,13 @@
display: none;
}
}
+
+ &:not(.is-collapsed) {
+ .board-list-component {
+ display: flex;
+ flex-direction: column;
+ }
+ }
}
.board-inner {
@@ -171,6 +175,7 @@
background: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
+ flex: 1;
}
.board-header {
@@ -232,9 +237,11 @@
}
.board-blank-state {
- height: calc(100% - 49px);
padding: $gl-padding;
background-color: $white-light;
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
}
.board-blank-state-list {
@@ -256,9 +263,9 @@
}
.board-list-component {
- height: calc(100% - 49px);
- overflow: hidden;
position: relative;
+ flex: 1;
+ min-height: 0; // firefox fix
}
.board-list {
@@ -281,7 +288,7 @@
padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
- border: 1px solid $theme-gray-200;
+ border: 1px solid $gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
line-height: $gl-padding;
@@ -489,7 +496,6 @@
}
.add-issues-modal {
- display: -webkit-flex;
display: flex;
position: fixed;
top: 0;
@@ -501,9 +507,7 @@
}
.add-issues-container {
- display: -webkit-flex;
display: flex;
- -webkit-flex-direction: column;
flex-direction: column;
width: 90vw;
height: 85vh;
@@ -516,16 +520,12 @@
box-shadow: 0 2px 12px rgba($black, 0.5);
.empty-state {
- display: -webkit-flex;
display: flex;
- -webkit-flex: 1;
flex: 1;
margin-top: 0;
&.add-issues-empty-state-filter {
- -webkit-flex-direction: column;
flex-direction: column;
- -webkit-justify-content: center;
justify-content: center;
}
@@ -554,11 +554,9 @@
}
.add-issues-search {
- display: -webkit-flex;
display: flex;
.issues-filters {
- -webkit-flex: 1;
flex: 1;
}
}
@@ -576,9 +574,7 @@
}
.add-issues-list {
- display: -webkit-flex;
display: flex;
- -webkit-flex: 1;
flex: 1;
padding-top: 3px;
margin-left: -$gl-vert-padding;
@@ -597,7 +593,6 @@
}
.add-issues-list-loading {
- -webkit-align-self: center;
align-self: center;
width: 100%;
padding-left: $gl-vert-padding;
@@ -673,7 +668,7 @@
}
.board-card-info-icon {
- color: $theme-gray-600;
+ color: $gray-600;
margin-right: $gl-padding-4;
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index 38fec3f0aa8..ce0622b3d48 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -11,15 +11,24 @@
}
.divergence-graph {
+ $graph-side-width: 80px;
+ $graph-separator-width: 1px;
+
padding: 0 6px;
.graph-side {
position: relative;
- width: 80px;
+ width: $graph-side-width;
height: 22px;
padding: 5px 0 13px;
float: left;
+ &.full {
+ width: $graph-side-width * 2 + $graph-separator-width;
+ display: flex;
+ justify-content: center;
+ }
+
.bar {
position: absolute;
height: 4px;
@@ -57,7 +66,7 @@
.graph-separator {
position: relative;
- width: 1px;
+ width: $graph-separator-width;
height: 18px;
margin: 5px 0 0;
float: left;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 09235661cea..fa5a182243c 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -55,16 +55,16 @@
@include build-trace();
}
- .archived-sticky {
+ .archived-job {
top: $header-height;
border-radius: 2px 2px 0 0;
color: $orange-600;
background-color: $orange-100;
border: 1px solid $border-gray-normal;
- border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
+ z-index: 1;
.with-performance-bar & {
top: $header-height + $performance-bar-height;
@@ -75,7 +75,11 @@
@include build-trace-top-bar(35px);
&.has-archived-block {
- top: $header-height + $performance-bar-height + 28px;
+ top: $header-height + 28px;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height + 28px;
+ }
}
&.affix {
@@ -261,7 +265,7 @@
.trigger-variables-table-cell {
font-size: $gl-font-size-small;
line-height: $gl-line-height;
- border: 1px solid $theme-gray-200;
+ border: 1px solid $gray-200;
padding: $gl-padding-4 6px;
width: 50%;
vertical-align: top;
@@ -272,7 +276,13 @@
}
.retry-link {
- display: none;
+ display: block;
+
+ .btn {
+ i {
+ margin-left: 5px;
+ }
+ }
.btn-inverted-secondary {
color: $blue-500;
@@ -281,16 +291,6 @@
color: $white-light;
}
}
-
- @include media-breakpoint-down(sm) {
- display: block;
-
- .btn {
- i {
- margin-left: 5px;
- }
- }
- }
}
.stage-item {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index ad12cd101b6..809ba6d4953 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -58,6 +58,20 @@
}
}
+.cluster-application-banner {
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.cluster-application-banner-close {
+ align-self: flex-start;
+ font-weight: 500;
+ font-size: 20px;
+ margin: $gl-padding-8 14px 0 0;
+}
+
.cluster-application-description {
flex: 1;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 11966931a6c..670e320dbc2 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -342,11 +342,11 @@
}
&.invalid {
- @include status-color($gray-dark, color("gray"), $gray-darkest);
+ @include status-color($gray-dark, color('gray'), $gray-darkest);
border-color: $gray-darkest;
&:not(span):hover {
- color: color("gray");
+ color: color('gray');
}
}
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index ec2108b15be..2b932d164a5 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -330,7 +330,6 @@
// Custom Styles for stage items
.item-build-component {
-
.item-title {
.icon-build-status {
float: left;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 37ed5ae674a..cb5f1a84005 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -34,7 +34,6 @@
.detail-page-header-actions {
align-self: center;
- flex-shrink: 0;
flex: 0 0 auto;
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 18c62cb4f1e..0dbbe9e4c25 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -7,21 +7,12 @@
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
- position: -webkit-sticky;
+ $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height;
+
position: sticky;
- top: 92px;
- margin-left: -1px;
- border-left: 1px solid $border-color;
+ top: $mr-file-header-top;
z-index: 102;
- &.is-commit {
- top: $header-height + 36px;
-
- .with-performance-bar & {
- top: $header-height + 36px + $performance-bar-height;
- }
- }
-
&::before {
content: '';
position: absolute;
@@ -34,7 +25,23 @@
}
.with-performance-bar & {
- top: 127px;
+ top: $mr-file-header-top + $performance-bar-height;
+ }
+
+ &.is-commit {
+ top: $header-height + $commit-stat-summary-height;
+
+ .with-performance-bar & {
+ top: $header-height + $commit-stat-summary-height + $performance-bar-height;
+ }
+ }
+
+ &.is-compare {
+ top: $header-height + $compare-branches-sticky-header-height;
+
+ .with-performance-bar & {
+ top: $performance-bar-height + $header-height + $compare-branches-sticky-header-height;
+ }
}
}
@@ -231,22 +238,18 @@
img {
border: 1px solid $white-light;
- background-image: linear-gradient(
- 45deg,
- $border-color 25%,
- transparent 25%,
- transparent 75%,
- $border-color 75%,
- $border-color 100%
- ),
- linear-gradient(
- 45deg,
- $border-color 25%,
- transparent 25%,
- transparent 75%,
- $border-color 75%,
- $border-color 100%
- );
+ background-image: linear-gradient(45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%),
+ linear-gradient(45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%);
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -500,6 +503,25 @@
}
}
+.diff-stats {
+ align-items: center;
+ padding: 0 0.25rem;
+
+ .diff-stats-group {
+ padding: 0 0.25rem;
+ }
+
+ svg.diff-stats-icon {
+ vertical-align: text-bottom;
+ }
+
+ &.is-compare-versions-header {
+ .diff-stats-group {
+ padding: 0 0.5rem;
+ }
+ }
+}
+
.file-content .diff-file {
margin: 0;
border: 0;
@@ -575,18 +597,6 @@
}
}
-@mixin diff_background($background, $idiff, $border) {
- background: $background;
-
- &.line_content span.idiff {
- background: $idiff;
- }
-
- &.diff-line-num {
- border-color: $border;
- }
-}
-
.files {
.diff-file:last-child {
margin-bottom: 0;
@@ -711,7 +721,6 @@
}
@include media-breakpoint-up(sm) {
- position: -webkit-sticky;
position: sticky;
top: $header-height;
background-color: $white-light;
@@ -819,34 +828,26 @@
width: 100%;
height: 10px;
background-color: $white-light;
- background-image: linear-gradient(
- 45deg,
- transparent,
- transparent 73%,
- $diff-jagged-border-gradient-color 75%,
- $white-light 80%
- ),
- linear-gradient(
- 225deg,
- transparent,
- transparent 73%,
- $diff-jagged-border-gradient-color 75%,
- $white-light 80%
- ),
- linear-gradient(
- 135deg,
- transparent,
- transparent 73%,
- $diff-jagged-border-gradient-color 75%,
- $white-light 80%
- ),
- linear-gradient(
- -45deg,
- transparent,
- transparent 73%,
- $diff-jagged-border-gradient-color 75%,
- $white-light 80%
- );
+ background-image: linear-gradient(45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%),
+ linear-gradient(225deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%),
+ linear-gradient(135deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%),
+ linear-gradient(-45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%);
background-position: 5px 5px, 0 5px, 0 5px, 5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
@@ -889,7 +890,7 @@
}
}
-.files:not([data-can-create-note="true"]) .frame {
+.files:not([data-can-create-note='true']) .frame {
cursor: auto;
}
@@ -898,15 +899,14 @@
.btn-transparent.image-diff-overlay-add-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
// Retina cursor
- cursor: -webkit-image-set(
- image-url('illustrations/image_comment_light_cursor.svg') 1x,
- image-url('illustrations/image_comment_light_cursor@2x.svg') 2x
- )
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ // scss-lint:disable DuplicateProperty
+ cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x,
+ image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
.comment-indicator {
@@ -1011,12 +1011,29 @@
}
.diff-tree-list {
- width: 320px;
+ position: sticky;
+ $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
+ top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
+ max-height: calc(100vh - #{$top-pos});
+ padding-right: $gl-padding;
+ z-index: 202;
+
+ .with-performance-bar & {
+ $performance-bar-top-pos: $performance-bar-height + $top-pos;
+ top: $performance-bar-top-pos;
+ max-height: calc(100vh - #{$performance-bar-top-pos});
+ }
+
+ .drag-handle {
+ bottom: 16px;
+ transform: translateX(-6px);
+ }
}
.diff-files-holder {
flex: 1;
min-width: 0;
+ z-index: 201;
}
.compare-versions-container {
@@ -1024,21 +1041,12 @@
}
.tree-list-holder {
- position: -webkit-sticky;
- position: sticky;
- top: 100px;
- max-height: calc(100vh - 100px);
- padding-right: $gl-padding;
+ height: 100%;
.file-row {
margin-left: 0;
margin-right: 0;
}
-
- .with-performance-bar & {
- top: 135px;
- max-height: calc(100vh - 135px);
- }
}
.tree-list-scroll {
@@ -1092,12 +1100,6 @@
}
}
-.tree-list-view-toggle {
- svg {
- top: 0;
- }
-}
-
.image-diff-overlay,
.image-diff-overlay-add-comment {
top: 0;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index f46ff360496..655b297295a 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -128,6 +128,10 @@
width: 100%;
}
}
+
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
+ clear: both;
+ }
}
.editor-ref {
@@ -178,9 +182,8 @@
.template-selector-dropdowns-wrap {
display: inline-block;
- margin-left: 8px;
- vertical-align: top;
margin: 5px 0 0 8px;
+ vertical-align: top;
@media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 75166ffcada..61ecf133b02 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -240,18 +240,7 @@
}
.prometheus-graph {
- flex: 1 0 auto;
- min-width: 450px;
- max-width: 100%;
padding: $gl-padding / 2;
-
- h5 {
- font-size: 16px;
- }
-
- @include media-breakpoint-down(sm) {
- min-width: 100%;
- }
}
.prometheus-graph-header {
@@ -261,13 +250,14 @@
margin-bottom: $gl-padding-8;
h5 {
+ font-size: $gl-font-size-large;
margin: 0;
}
}
.prometheus-graph-cursor {
position: absolute;
- background: $theme-gray-600;
+ background: $gray-600;
width: 1px;
}
@@ -309,7 +299,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-left: 4px solid $theme-gray-50;
+ border-left: 4px solid $gray-50;
}
.arrow-shadow {
@@ -331,7 +321,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-right: 4px solid $theme-gray-50;
+ border-right: 4px solid $gray-50;
}
.arrow-shadow {
@@ -364,7 +354,7 @@
}
> .popover-title {
- background-color: $theme-gray-50;
+ background-color: $gray-50;
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
@@ -439,7 +429,7 @@
}
> text {
- fill: $theme-gray-600;
+ fill: $gray-600;
font-size: 10px;
}
}
@@ -482,5 +472,5 @@
}
.prometheus-table-row-highlight {
- background-color: $theme-gray-100;
+ background-color: $gray-100;
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 4fb1a956fab..3febf4cf826 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -44,7 +44,7 @@
}
.x-axis-text {
- fill: $theme-gray-900;
+ fill: $gray-900;
}
.bar-rect {
@@ -64,19 +64,17 @@
text {
font-weight: bold;
font-size: 12px;
- fill: $theme-gray-800;
+ fill: $gray-800;
}
}
}
.svg-graph-container-with-grab {
cursor: grab;
- cursor: -webkit-grab;
}
.svg-graph-container-grabbed {
cursor: grabbing;
- cursor: -webkit-grabbing;
}
@keyframes flickerAnimation {
@@ -87,5 +85,5 @@
.animate-flicker {
animation: flickerAnimation 1.5s infinite;
- fill: $theme-gray-500;
+ fill: $gray-500;
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index f0cb81e0bc3..8ade995525a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -29,9 +29,7 @@
}
}
-.group-nav-container .group-search,
.group-nav-container .nav-controls {
- display: flex;
align-items: flex-start;
padding: $gl-padding-top 0 0;
@@ -44,6 +42,52 @@
margin-top: 0;
}
+ @include media-breakpoint-down(sm) {
+ .dropdown,
+ .dropdown .dropdown-toggle,
+ .btn-success {
+ display: block;
+ }
+
+ .group-filter-form,
+ .dropdown {
+ margin-bottom: 10px;
+ margin-right: 0;
+ }
+
+ &,
+ .group-filter-form,
+ .group-filter-form-field,
+ .dropdown,
+ .dropdown .dropdown-toggle,
+ .btn-success {
+ width: 100%;
+ }
+
+ .dropdown .dropdown-toggle .fa-chevron-down {
+ position: absolute;
+ top: 11px;
+ right: 8px;
+ }
+ }
+}
+
+.home-panel-buttons {
+ .home-panel-action-button {
+ vertical-align: top;
+ }
+
+
+ .notification-dropdown {
+ .dropdown-menu {
+ @extend .dropdown-menu-right;
+ }
+
+ .icon {
+ fill: $gl-text-color-secondary;
+ }
+ }
+
.new-project-subgroup {
.dropdown-primary {
min-width: 115px;
@@ -78,7 +122,7 @@
&:hover {
background-color: $gray-darker;
- color: $theme-gray-900;
+ color: $gray-900;
}
}
@@ -99,61 +143,29 @@
font-weight: $gl-font-weight-bold;
}
}
- }
- }
-
- @include media-breakpoint-down(sm) {
- &,
- .dropdown,
- .dropdown .dropdown-toggle,
- .btn-success {
- display: block;
- }
- .group-filter-form,
- .dropdown {
- margin-bottom: 10px;
- margin-right: 0;
- }
-
- .group-filter-form,
- .dropdown .dropdown-toggle,
- .btn-success {
- width: 100%;
- }
-
- .dropdown .dropdown-toggle .fa-chevron-down {
- position: absolute;
- top: 11px;
- right: 8px;
- }
-
- .new-project-subgroup {
- display: flex;
- align-items: flex-start;
+ @include media-breakpoint-down(sm) {
+ display: flex;
+ align-items: flex-start;
- .dropdown-primary {
- flex: 1;
- }
+ .dropdown-primary {
+ flex: 1;
+ }
- .dropdown-toggle {
- width: auto;
- }
+ .dropdown-toggle {
+ width: auto;
+ }
- .dropdown-menu {
- width: 100%;
- max-width: inherit;
- min-width: inherit;
+ .dropdown-menu {
+ width: 100%;
+ max-width: inherit;
+ min-width: inherit;
+ }
}
}
}
}
-.group-nav-container .group-search {
- padding: $gl-padding 0;
- border-bottom: 1px solid $border-color;
-}
-
.groups-listing {
.group-list-tree .group-row:first-child {
border-top: 0;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 2c23f31c240..161d4dbfb22 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -30,7 +30,7 @@
.key {
@extend .badge.badge-pill;
background-color: $label-inverse-bg;
- font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace;
padding: 3px 5px;
}
}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index a4f76a9495a..7f800367cad 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,20 +1,51 @@
-.import-jobs-from-col,
.import-jobs-to-col {
- width: 40%;
+ width: 39%;
}
.import-jobs-status-col {
- width: 20%;
+ width: 15%;
}
-.btn-import {
- .loading-icon {
- display: none;
+.import-jobs-cta-col {
+ width: 1%;
+}
+
+.import-project-name-input {
+ border-radius: 0 $border-radius-default $border-radius-default 0;
+ position: relative;
+ left: -1px;
+ max-width: 300px;
+}
+
+.import-namespace-select {
+ width: auto !important;
+
+ > .select2-choice {
+ border-radius: $border-radius-default 0 0 $border-radius-default;
+ position: relative;
+ left: 1px;
}
+}
- &.is-loading {
- .loading-icon {
- display: inline-block;
- }
+.import-slash-divider {
+ background-color: $gray-lightest;
+ border: 1px solid $border-color;
+}
+
+.import-row {
+ height: 55px;
+}
+
+.import-table {
+ .import-jobs-from-col,
+ .import-jobs-to-col,
+ .import-jobs-status-col,
+ .import-jobs-cta-col {
+ border-bottom-width: 1px;
+ padding-left: $gl-padding;
}
}
+
+.import-projects-loading-icon {
+ margin-top: $gl-padding-32;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 5b5f486ea63..623fa485ba6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -22,6 +22,7 @@
.detail-page-header,
.page-content-header,
.commit-box,
+ .info-well,
.commit-ci-menu,
.files-changed-inner,
.limited-header-width,
@@ -60,6 +61,10 @@
padding: 0;
margin-bottom: $gl-padding;
border-bottom: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ min-width: 0;
+ width: 100%;
}
.btn-edit {
@@ -132,7 +137,7 @@
color: $blue-800;
.avatar {
- border-color: rgba($gray-normal, .2);
+ border-color: rgba($gray-normal, 0.2);
}
}
@@ -219,7 +224,7 @@
}
a.edit-link:not([href]):hover {
- color: rgba($gray-normal, .2);
+ color: rgba($gray-normal, 0.2);
}
.lock-edit, // uses same style, different js behaviour
@@ -707,14 +712,11 @@
.issuable-list {
li {
-
.issue-box {
- display: -webkit-flex;
display: flex;
}
.issuable-info-container {
- -webkit-flex: 1;
flex: 1;
display: flex;
padding-right: $gl-padding;
@@ -809,7 +811,6 @@
.sidebar-collapsed-icon {
-
> .stopwatch-svg {
display: inline-block;
}
@@ -867,11 +868,11 @@
}
.help-state-toggle-enter-active {
- transition: all .8s ease;
+ transition: all 0.8s ease;
}
.help-state-toggle-leave-active {
- transition: all .5s ease;
+ transition: all 0.5s ease;
}
.help-state-toggle-enter,
@@ -930,7 +931,7 @@
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
- color: $theme-gray-700;
+ color: $gray-700;
+ .sidebar-collapsed-icon {
padding-top: 0;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index bb6b6f84849..9f30495a7ef 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -58,8 +58,6 @@ form.edit-issue {
}
ul.related-merge-requests > li {
- display: -ms-flexbox;
- display: -webkit-flex;
display: flex;
align-items: center;
@@ -80,7 +78,6 @@ ul.related-merge-requests > li {
}
}
-.merge-requests-title,
.related-branches-title {
font-size: 16px;
font-weight: $gl-font-weight-bold;
@@ -91,23 +88,16 @@ ul.related-merge-requests > li {
}
.merge-request-status {
- font-size: 13px;
- padding: 0 5px;
- color: $white-light;
- height: 20px;
- border-radius: 3px;
- line-height: 18px;
-
&.merged {
- background: $blue-500;
+ color: $blue-500;
}
&.closed {
- background: $red-500;
+ color: $red-500;
}
&.open {
- background: $green-500;
+ color: $green-500;
}
}
@@ -155,6 +145,14 @@ ul.related-merge-requests > li {
}
}
+.issues-nav-controls {
+ font-size: 0;
+
+ .btn-group:empty {
+ display: none;
+ }
+}
+
.issuable-email-modal-btn {
padding: 0;
color: $blue-600;
diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss
index 4fba89e956b..64ca61f7094 100644
--- a/app/assets/stylesheets/pages/issues/issue_count_badge.scss
+++ b/app/assets/stylesheets/pages/issues/issue_count_badge.scss
@@ -1,11 +1,13 @@
-.issue-count-badge {
+.issue-count-badge,
+.mr-count-badge {
display: inline-flex;
border-radius: $border-radius-base;
border: 1px solid $border-color;
padding: 5px $gl-padding-8;
}
-.issue-count-badge-count {
+.issue-count-badge-count,
+.mr-count-badge-count {
display: inline-flex;
align-items: center;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index d2b9470be69..75d219320ef 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -82,7 +82,7 @@
justify-content: space-between;
padding: $gl-padding;
border-radius: $border-radius-default;
- border: 1px solid $theme-gray-100;
+ border: 1px solid $gray-100;
&:last-child {
margin-bottom: 0;
@@ -95,13 +95,11 @@
.prioritized-labels & {
box-shadow: 0 1px 2px $issue-boards-card-shadow;
cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
+ cursor: grab;
border: 0;
&:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
+ cursor: grabbing;
}
}
}
@@ -257,7 +255,7 @@
}
.label-badge {
- color: $theme-gray-900;
+ color: $gray-900;
font-weight: $gl-font-weight-normal;
padding: $gl-padding-4 $gl-padding-8;
border-radius: $border-radius-default;
@@ -269,7 +267,7 @@
}
.label-badge-gray {
- background-color: $theme-gray-100;
+ background-color: $gray-100;
}
.label-links {
@@ -326,11 +324,11 @@
}
.label-action {
- color: $theme-gray-800;
+ color: $gray-800;
cursor: pointer;
svg {
- fill: $theme-gray-800;
+ fill: $gray-800;
}
&:hover {
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 67d7a8175ac..22a515cbdaa 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -120,7 +120,6 @@
}
.new-session-tabs {
- display: -webkit-flex;
display: flex;
box-shadow: 0 0 0 1px $border-color;
border-top-right-radius: $border-radius-default;
@@ -190,7 +189,7 @@
margin-top: 16px;
}
- input[type="submit"] {
+ input[type='submit'] {
@extend .btn-block;
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 99609a96976..eb32beb0972 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -27,7 +27,6 @@
.controls {
@include media-breakpoint-up(sm) {
- display: -webkit-flex;
display: flex;
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index e0f7d075fc7..278a9014458 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -20,81 +20,81 @@ $colors: (
white-header-not-chosen : #f0f0f0,
white-line-not-chosen : $gray-light,
- dark-header-head-neutral : rgba(#3f3, .2),
- dark-line-head-neutral : rgba(#3f3, .1),
+ dark-header-head-neutral : rgba(#3f3, 0.2),
+ dark-line-head-neutral : rgba(#3f3, 0.1),
dark-button-head-neutral : #40874f,
- dark-header-head-chosen : rgba(#3f3, .33),
- dark-line-head-chosen : rgba(#3f3, .2),
+ dark-header-head-chosen : rgba(#3f3, 0.33),
+ dark-line-head-chosen : rgba(#3f3, 0.2),
dark-button-head-chosen : #258537,
- dark-header-origin-neutral : rgba(#2878c9, .4),
- dark-line-origin-neutral : rgba(#2878c9, .3),
+ dark-header-origin-neutral : rgba(#2878c9, 0.4),
+ dark-line-origin-neutral : rgba(#2878c9, 0.3),
dark-button-origin-neutral : #2a5c8c,
- dark-header-origin-chosen : rgba(#2878c9, .6),
- dark-line-origin-chosen : rgba(#2878c9, .4),
+ dark-header-origin-chosen : rgba(#2878c9, 0.6),
+ dark-line-origin-chosen : rgba(#2878c9, 0.4),
dark-button-origin-chosen : #1d6cbf,
- dark-header-not-chosen : rgba(#fff, .25),
- dark-line-not-chosen : rgba(#fff, .1),
+ dark-header-not-chosen : rgba(#fff, 0.25),
+ dark-line-not-chosen : rgba(#fff, 0.1),
- monokai-header-head-neutral : rgba(#a6e22e, .25),
- monokai-line-head-neutral : rgba(#a6e22e, .1),
+ monokai-header-head-neutral : rgba(#a6e22e, 0.25),
+ monokai-line-head-neutral : rgba(#a6e22e, 0.1),
monokai-button-head-neutral : #376b20,
- monokai-header-head-chosen : rgba(#a6e22e, .4),
- monokai-line-head-chosen : rgba(#a6e22e, .25),
+ monokai-header-head-chosen : rgba(#a6e22e, 0.4),
+ monokai-line-head-chosen : rgba(#a6e22e, 0.25),
monokai-button-head-chosen : #39800d,
- monokai-header-origin-neutral : rgba(#60d9f1, .35),
- monokai-line-origin-neutral : rgba(#60d9f1, .15),
+ monokai-header-origin-neutral : rgba(#60d9f1, 0.35),
+ monokai-line-origin-neutral : rgba(#60d9f1, 0.15),
monokai-button-origin-neutral : #38848c,
- monokai-header-origin-chosen : rgba(#60d9f1, .5),
- monokai-line-origin-chosen : rgba(#60d9f1, .35),
+ monokai-header-origin-chosen : rgba(#60d9f1, 0.5),
+ monokai-line-origin-chosen : rgba(#60d9f1, 0.35),
monokai-button-origin-chosen : #3ea4b2,
- monokai-header-not-chosen : rgba(#76715d, .24),
- monokai-line-not-chosen : rgba(#76715d, .1),
+ monokai-header-not-chosen : rgba(#76715d, 0.24),
+ monokai-line-not-chosen : rgba(#76715d, 0.1),
- solarized-light-header-head-neutral : rgba(#859900, .37),
- solarized-light-line-head-neutral : rgba(#859900, .2),
+ solarized-light-header-head-neutral : rgba(#859900, 0.37),
+ solarized-light-line-head-neutral : rgba(#859900, 0.2),
solarized-light-button-head-neutral : #afb262,
- solarized-light-header-head-chosen : rgba(#859900, .5),
- solarized-light-line-head-chosen : rgba(#859900, .37),
+ solarized-light-header-head-chosen : rgba(#859900, 0.5),
+ solarized-light-line-head-chosen : rgba(#859900, 0.37),
solarized-light-button-head-chosen : #94993d,
- solarized-light-header-origin-neutral : rgba(#2878c9, .37),
- solarized-light-line-origin-neutral : rgba(#2878c9, .15),
+ solarized-light-header-origin-neutral : rgba(#2878c9, 0.37),
+ solarized-light-line-origin-neutral : rgba(#2878c9, 0.15),
solarized-light-button-origin-neutral : #60a1bf,
- solarized-light-header-origin-chosen : rgba(#2878c9, .6),
- solarized-light-line-origin-chosen : rgba(#2878c9, .37),
+ solarized-light-header-origin-chosen : rgba(#2878c9, 0.6),
+ solarized-light-line-origin-chosen : rgba(#2878c9, 0.37),
solarized-light-button-origin-chosen : #2482b2,
- solarized-light-header-not-chosen : rgba(#839496, .37),
- solarized-light-line-not-chosen : rgba(#839496, .2),
+ solarized-light-header-not-chosen : rgba(#839496, 0.37),
+ solarized-light-line-not-chosen : rgba(#839496, 0.2),
- solarized-dark-header-head-neutral : rgba(#859900, .35),
- solarized-dark-line-head-neutral : rgba(#859900, .15),
+ solarized-dark-header-head-neutral : rgba(#859900, 0.35),
+ solarized-dark-line-head-neutral : rgba(#859900, 0.15),
solarized-dark-button-head-neutral : #376b20,
- solarized-dark-header-head-chosen : rgba(#859900, .5),
- solarized-dark-line-head-chosen : rgba(#859900, .35),
+ solarized-dark-header-head-chosen : rgba(#859900, 0.5),
+ solarized-dark-line-head-chosen : rgba(#859900, 0.35),
solarized-dark-button-head-chosen : #39800d,
- solarized-dark-header-origin-neutral : rgba(#2878c9, .35),
- solarized-dark-line-origin-neutral : rgba(#2878c9, .15),
+ solarized-dark-header-origin-neutral : rgba(#2878c9, 0.35),
+ solarized-dark-line-origin-neutral : rgba(#2878c9, 0.15),
solarized-dark-button-origin-neutral : #086799,
- solarized-dark-header-origin-chosen : rgba(#2878c9, .6),
- solarized-dark-line-origin-chosen : rgba(#2878c9, .35),
+ solarized-dark-header-origin-chosen : rgba(#2878c9, 0.6),
+ solarized-dark-line-origin-chosen : rgba(#2878c9, 0.35),
solarized-dark-button-origin-chosen : #0082cc,
- solarized_dark_header_not_chosen : rgba(#839496, .25),
- solarized_dark_line_not_chosen : rgba(#839496, .15),
+ solarized_dark_header_not_chosen : rgba(#839496, 0.25),
+ solarized_dark_line_not_chosen : rgba(#839496, 0.15),
none_header_head_neutral : $gray-normal,
none_line_head_neutral : $gray-normal,
@@ -210,26 +210,20 @@ $colors: (
}
#conflicts {
-
.white {
- @include color-scheme('white')
- }
+ @include color-scheme('white'); }
.dark {
- @include color-scheme('dark')
- }
+ @include color-scheme('dark'); }
.monokai {
- @include color-scheme('monokai')
- }
+ @include color-scheme('monokai'); }
.solarized-light {
- @include color-scheme('solarized-light')
- }
+ @include color-scheme('solarized-light'); }
.solarized-dark {
- @include color-scheme('solarized-dark')
- }
+ @include color-scheme('solarized-dark'); }
.diff-wrap-lines .line_content {
white-space: normal;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 221b4e934ff..e73d1a1289d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -38,9 +38,7 @@
}
.mr-widget-section {
- .media {
- align-items: center;
- }
+ border-radius: $border-radius-default $border-radius-default 0 0;
.code-text {
flex: 1;
@@ -56,6 +54,11 @@
.mr-widget-extension {
border-top: 1px solid $border-color;
background-color: $gray-light;
+
+ &.clickable:hover {
+ background-color: $gl-gray-200;
+ cursor: pointer;
+ }
}
.mr-widget-workflow {
@@ -64,7 +67,7 @@
&::before {
content: '';
- border-left: 1px solid $theme-gray-200;
+ border-left: 1px solid $gray-200;
position: absolute;
left: 32px;
top: -17px;
@@ -78,7 +81,7 @@
border-top: 0;
}
-.mr-widget-section,
+.mr-widget-body,
.mr-widget-content,
.mr-widget-footer {
padding: $gl-padding;
@@ -87,11 +90,38 @@
.mr-state-widget {
color: $gl-text-color;
+ .commit-message-edit {
+ border-radius: $border-radius-default;
+ }
+
.mr-widget-section,
.mr-widget-footer {
border-top: solid 1px $border-color;
}
+ .mr-fast-forward-message {
+ padding-left: $gl-padding-50;
+ padding-bottom: $gl-padding;
+ }
+
+ .commits-list {
+ > li {
+ padding: $gl-padding;
+
+ @include media-breakpoint-up(md) {
+ padding-left: $gl-padding-50;
+ }
+ }
+ }
+
+ .mr-commit-dropdown {
+ .dropdown-menu {
+ @include media-breakpoint-up(md) {
+ width: 150%;
+ }
+ }
+ }
+
.mr-widget-footer {
padding: 0;
}
@@ -405,7 +435,7 @@
}
.mr-widget-help {
- padding: 10px 16px 10px 48px;
+ padding: 10px 16px 10px $gl-padding-50;
font-style: italic;
}
@@ -423,10 +453,6 @@
}
}
-.mr-widget-body-controls {
- flex-wrap: wrap;
-}
-
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
@@ -465,11 +491,9 @@
.merge-request {
padding: 10px 0 10px 15px;
position: relative;
- display: -webkit-flex;
display: flex;
.issuable-info-container {
- -webkit-flex: 1;
flex: 1;
}
@@ -708,8 +732,11 @@
.mr-version-controls {
position: relative;
+ z-index: 203;
background: $gray-light;
color: $gl-text-color;
+ margin-top: -1px;
+ border-top: 1px solid $border-color;
.mr-version-menus-container {
display: flex;
@@ -755,17 +782,35 @@
color: $orange-500;
padding-right: 5px;
}
+
+ @include media-breakpoint-up(md) {
+ position: sticky;
+ top: $header-height + $mr-tabs-height;
+ width: 100%;
+
+ &.is-fileTreeOpen {
+ margin-left: -16px;
+ width: calc(100% + 32px);
+ }
+
+ .mr-version-menus-container {
+ flex-wrap: nowrap;
+ }
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height + $mr-tabs-height;
+ }
+ }
}
.merge-request-tabs-holder {
top: $header-height;
- z-index: 200;
+ z-index: 300;
background-color: $white-light;
border-bottom: 1px solid $border-color;
@include media-breakpoint-up(sm) {
position: sticky;
- position: -webkit-sticky;
}
&.affix {
@@ -816,7 +861,7 @@
display: flex;
justify-content: space-between;
- @include media-breakpoint-down(md) {
+ @include media-breakpoint-down(sm) {
flex-direction: column-reverse;
}
@@ -907,7 +952,7 @@
}
.btn svg {
- fill: $theme-gray-700;
+ fill: $gray-700;
}
.dropdown-menu {
@@ -951,7 +996,7 @@
.coverage {
font-size: 12px;
- color: $theme-gray-700;
+ color: $gray-700;
line-height: initial;
}
@@ -961,3 +1006,14 @@
width: $ci-action-icon-size-lg;
}
}
+
+.merge-request-details .file-finder-overlay.diff-file-finder {
+ position: fixed;
+ z-index: 99999;
+ background: $black-transparent;
+}
+
+.source-branch-removal-status {
+ padding-left: 50px;
+ padding-bottom: $gl-padding;
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 1e92582d6d9..3ca8e943a3a 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -1,3 +1,5 @@
+$status-box-line-height: 26px;
+
.issues-sortable-list .str-truncated {
max-width: 90%;
}
@@ -6,7 +8,7 @@
padding: $gl-padding-8;
margin-top: $gl-padding-8;
border-radius: $border-radius-default;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
.milestone {
border: 0;
@@ -38,6 +40,7 @@
font-size: $tooltip-font-size;
margin-top: 0;
margin-right: $gl-padding-4;
+ line-height: $status-box-line-height;
@include media-breakpoint-down(xs) {
line-height: unset;
@@ -64,18 +67,14 @@
.card-header {
line-height: $line-height-base;
padding: 14px 16px;
- display: -webkit-flex;
display: flex;
.title {
- -webkit-flex: 1;
- -webkit-flex-grow: 1;
flex: 1;
flex-grow: 2;
}
.counter {
- -webkit-flex: 1;
flex: 0;
padding-left: 16px;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 5b30295adf9..3343b55d24b 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -1,10 +1,6 @@
/**
* Note Form
*/
-.comment-btn {
- @extend .btn-success;
-}
-
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
opacity: 1;
@@ -62,7 +58,7 @@
border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
- box-shadow ease-in-out 0.15s;
+ box-shadow ease-in-out 0.15s;
&.is-focused {
@extend .form-control:focus;
@@ -76,7 +72,7 @@
&.is-dropzone-hover {
border-color: $green-500;
box-shadow: 0 0 2px $black-transparent,
- 0 0 4px $green-500-focus;
+ 0 0 4px $green-500-focus;
.comment-toolbar,
.nav-links {
@@ -88,9 +84,7 @@
.md-header .nav-links {
display: flex;
- display: -webkit-flex;
flex-flow: row wrap;
- -webkit-flex-flow: row wrap;
width: 100%;
.float-right {
@@ -151,7 +145,7 @@
}
.sidebar-item-value & {
- fill: $theme-gray-700;
+ fill: $gray-700;
}
}
@@ -386,7 +380,7 @@ table {
}
.comment-type-dropdown {
- .comment-btn {
+ .btn-success {
width: auto;
}
@@ -417,7 +411,7 @@ table {
width: 100%;
margin-bottom: 10px;
- .comment-btn {
+ .btn-success {
flex-grow: 1;
flex-shrink: 0;
width: auto;
@@ -448,7 +442,7 @@ table {
.uploading-error-message {
@include media-breakpoint-down(xs) {
&::after {
- content: "\a";
+ content: '\a';
white-space: pre;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a5b1eff3e1d..72f48e98c24 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -5,7 +5,7 @@ $note-form-margin-left: 72px;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid $theme-gray-100;
+ border-left: 2px solid $gray-100;
position: absolute;
top: 0;
bottom: 0;
@@ -153,12 +153,12 @@ $note-form-margin-left: 72px;
position: relative;
.timeline-discussion-body {
- margin-top: -8px;
+ margin-top: -$gl-padding-8;
overflow-x: auto;
overflow-y: hidden;
- .discussion-resolved-text {
- margin-bottom: 8px;
+ .note-body {
+ margin-top: $gl-padding-8;
}
}
@@ -303,26 +303,6 @@ $note-form-margin-left: 72px;
}
}
- .timeline-icon {
- float: left;
- display: flex;
- align-items: center;
- background-color: $white-light;
- width: $system-note-icon-size;
- height: $system-note-icon-size;
- border: 1px solid $border-color;
- border-radius: $system-note-icon-size;
- margin: -6px $gl-padding 0 0;
-
- svg {
- width: $system-note-svg-size;
- height: $system-note-svg-size;
- fill: $gray-darkest;
- display: block;
- margin: 0 auto;
- }
- }
-
.timeline-content {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 30px;
@@ -380,6 +360,37 @@ $note-form-margin-left: 72px;
}
}
}
+
+ .system-note,
+ .discussion-filter-note {
+ .timeline-icon {
+ float: left;
+ display: flex;
+ align-items: center;
+ background-color: $white-light;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
+ border: 1px solid $border-color;
+ border-radius: $system-note-icon-size;
+ margin: -6px $gl-padding 0 0;
+
+ svg {
+ width: $system-note-svg-size;
+ height: $system-note-svg-size;
+ fill: $gray-darkest;
+ display: block;
+ margin: 0 auto;
+ }
+ }
+ }
+
+ .discussion-filter-note {
+ .timeline-icon {
+ width: $system-note-icon-size + 6;
+ height: $system-note-icon-size + 6;
+ margin-top: -8px;
+ }
+ }
}
// Diff code in discussion view
@@ -494,11 +505,6 @@ $note-form-margin-left: 72px;
.discussion-notes {
margin-left: 0;
border-left: 0;
-
- .notes {
- position: relative;
- @include vertical-line(52px);
- }
}
.note-wrapper {
@@ -550,6 +556,11 @@ $note-form-margin-left: 72px;
.note-header-info {
padding-bottom: 0;
}
+
+ .timeline-content {
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
}
.unresolved {
@@ -597,7 +608,6 @@ $note-form-margin-left: 72px;
.note-headline-meta {
display: inline-block;
- white-space: nowrap;
.system-note-message {
white-space: normal;
@@ -607,6 +617,10 @@ $note-form-margin-left: 72px;
color: $gl-text-color-disabled;
}
+ .note-timestamp {
+ white-space: nowrap;
+ }
+
a:hover {
text-decoration: underline;
}
@@ -902,7 +916,6 @@ $note-form-margin-left: 72px;
}
.discussion-filter-container {
-
.btn > svg {
width: $gl-col-padding;
height: $gl-col-padding;
@@ -924,7 +937,6 @@ $note-form-margin-left: 72px;
//This needs to be deleted when Snippet/Commit comments are convered to Vue
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/53918#note_117038785
.unstyled-comments {
-
.discussion-header {
padding: $gl-padding;
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index fdd17af35fb..acc562f0769 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -256,14 +256,25 @@
}
}
- .mini-pipeline-graph-dropdown-toggle svg {
- height: $ci-action-icon-size;
- width: $ci-action-icon-size;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
+ .mini-pipeline-graph-dropdown-toggle {
+ svg {
+ height: $ci-action-icon-size;
+ width: $ci-action-icon-size;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
+
+ &:hover,
+ &:active,
+ &:focus {
+ svg {
+ top: -2px;
+ left: -2px;
+ }
+ }
}
.stage-container {
@@ -293,7 +304,7 @@
width: 7px;
position: absolute;
right: -7px;
- top: 10px;
+ top: 11px;
border-bottom: 2px solid $border-color;
}
}
@@ -433,6 +444,7 @@
}
.pipeline-tab-content {
+ display: flex;
width: 100%;
background-color: $gray-light;
padding: $gl-padding;
@@ -707,21 +719,43 @@
font-weight: $gl-font-weight-normal;
}
-@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) {
- border-color: $color-main;
- color: $color-main;
+@mixin mini-pipeline-graph-color(
+ $color-background-default,
+ $color-background-hover-focus,
+ $color-background-active,
+ $color-foreground-default,
+ $color-foreground-hover-focus,
+ $color-foreground-active
+) {
+ background-color: $color-background-default;
+ border-color: $color-foreground-default;
+
+ svg {
+ fill: $color-foreground-default;
+ }
&:hover,
- &:focus,
+ &:focus {
+ background-color: $color-background-hover-focus;
+ border-color: $color-foreground-hover-focus;
+
+ svg {
+ fill: $color-foreground-hover-focus;
+ }
+ }
+
&:active {
- background-color: $color-light;
- border-color: $color-dark;
- color: $color-dark;
+ background-color: $color-background-active;
+ border-color: $color-foreground-active;
svg {
- fill: $color-dark;
+ fill: $color-foreground-active;
}
}
+
+ &:focus {
+ box-shadow: 0 0 4px 1px $blue-300;
+ }
}
@mixin mini-pipeline-item() {
@@ -733,26 +767,32 @@
height: $ci-action-icon-size;
margin: 0;
padding: 0;
- transition: all 0.2s linear;
position: relative;
vertical-align: middle;
+ &:hover,
+ &:active,
+ &:focus {
+ outline: none;
+ border-width: 2px;
+ }
+
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
- @include mini-pipeline-graph-color($green-100, $green-500, $green-600);
+ @include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700);
}
&.ci-status-icon-failed {
- @include mini-pipeline-graph-color($red-100, $red-500, $red-600);
+ @include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700);
}
&.ci-status-icon-pending,
&.ci-status-icon-success_with_warnings {
- @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600);
+ @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700);
}
&.ci-status-icon-running {
- @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600);
+ @include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
}
&.ci-status-icon-canceled,
@@ -760,42 +800,18 @@
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
- @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color);
+ @include mini-pipeline-graph-color($white, $gray-700, $gray-800, $gray-900, $gray-950, $black);
}
&.ci-status-icon-created,
&.ci-status-icon-skipped {
- @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest);
+ @include mini-pipeline-graph-color($white, $gray-200, $gray-300, $gray-500, $gray-600, $gray-700);
}
}
// Dropdown button in mini pipeline graph
button.mini-pipeline-graph-dropdown-toggle {
@include mini-pipeline-item();
-
- > .fa.fa-caret-down {
- position: absolute;
- left: 20px;
- top: 5px;
- display: inline-block;
- visibility: hidden;
- opacity: 0;
- color: inherit;
- font-size: 12px;
- transition: visibility 0.1s, opacity 0.1s linear;
- }
-
- &:active,
- &:focus,
- &:hover {
- outline: none;
- width: 35px;
-
- .fa.fa-caret-down {
- visibility: visible;
- opacity: 1;
- }
- }
}
/**
@@ -845,7 +861,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
- top: 0;
+ top: 1px;
vertical-align: initial;
}
}
@@ -853,7 +869,7 @@ button.mini-pipeline-graph-dropdown-toggle {
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
- top: 2px;
+ top: 4px;
}
.scrollable-menu {
@@ -978,8 +994,6 @@ button.mini-pipeline-graph-dropdown-toggle {
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
- z-index: 200;
-
&::before,
&::after {
content: '';
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b813eb16dad..ab26259c007 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -249,6 +249,16 @@
.event-item {
padding-left: 40px;
}
+
+ @include media-breakpoint-up(lg) {
+ margin-right: 5px;
+ }
+ }
+
+ .projects-block {
+ @include media-breakpoint-up(lg) {
+ margin-left: 5px;
+ }
}
@include media-breakpoint-down(xs) {
@@ -260,11 +270,9 @@
position: static;
padding: 0 16px;
margin-bottom: 20px;
- display: -webkit-flex;
display: flex;
.btn {
- -webkit-flex-grow: 1;
flex-grow: 1;
&:first-child {
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index a353f301d07..45e62913f37 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -60,11 +60,11 @@
}
&.ui-dark {
- background-color: $theme-gray-900;
+ background-color: $gray-900;
}
&.ui-light {
- background-color: $theme-gray-200;
+ background-color: $gray-200;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0ce0db038a7..1349845f300 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -140,73 +140,19 @@
}
}
-.project-home-panel,
-.group-home-panel {
- padding-top: 24px;
- padding-bottom: 24px;
-
- .group-avatar {
- float: none;
- margin: 0 auto;
-
- &.identicon {
- border-radius: 50%;
- }
- }
-
- .group-title {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 24px;
- font-weight: $gl-font-weight-normal;
- line-height: 1;
- word-wrap: break-word;
-
- .fa {
- margin-left: 2px;
- font-size: 12px;
- vertical-align: middle;
- }
- }
-
- .group-home-desc {
- margin-left: auto;
- margin-right: auto;
- margin-bottom: 0;
- max-width: 700px;
-
- > p {
- margin-bottom: 0;
- }
- }
-
- .notifications-btn {
- .fa-bell,
- .fa-spinner {
- margin-right: 6px;
- }
-
- .fa-angle-down {
- margin-left: 6px;
- }
- }
-}
-
+.group-home-panel,
.project-home-panel {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
- .project-avatar {
- width: $project-title-row-height;
- height: $project-title-row-height;
+ .home-panel-avatar {
+ width: $home-panel-title-row-height;
+ height: $home-panel-title-row-height;
flex-shrink: 0;
- flex-basis: $project-title-row-height;
- margin: 0 $gl-padding 0 0;
+ flex-basis: $home-panel-title-row-height;
}
- .project-title {
- margin-top: 8px;
- margin-bottom: 5px;
+ .home-panel-title {
font-size: 20px;
line-height: $gl-line-height-24;
font-weight: bold;
@@ -215,11 +161,7 @@
font-size: $gl-font-size-large;
}
- .project-visibility {
- color: $gl-text-color-secondary;
- }
-
- .project-tag-list {
+ .home-panel-topic-list {
font-size: $gl-font-size;
font-weight: $gl-font-weight-normal;
@@ -231,12 +173,12 @@
}
}
- .project-title-row {
+ .home-panel-title-row {
@include media-breakpoint-down(sm) {
- .project-avatar {
- width: $project-avatar-mobile-size;
- height: $project-avatar-mobile-size;
- flex-basis: $project-avatar-mobile-size;
+ .home-panel-avatar {
+ width: $home-panel-avatar-mobile-size;
+ height: $home-panel-avatar-mobile-size;
+ flex-basis: $home-panel-avatar-mobile-size;
.avatar {
font-size: 20px;
@@ -244,28 +186,26 @@
}
}
- .project-title {
+ .home-panel-title {
margin-top: 4px;
margin-bottom: 2px;
font-size: $gl-font-size;
line-height: $gl-font-size-large;
}
- .project-tag-list,
- .project-metadata {
+ .home-panel-topic-list,
+ .home-panel-metadata {
font-size: $gl-font-size-small;
}
}
}
- .project-metadata {
+ .home-panel-metadata {
font-weight: normal;
font-size: 14px;
line-height: $gl-btn-line-height;
- color: $gl-text-color-secondary;
-
- .project-license {
+ .home-panel-license {
.btn {
line-height: 0;
border-width: 0;
@@ -273,13 +213,13 @@
}
.access-request-link,
- .project-tag-list {
+ .home-panel-topic-list {
padding-left: $gl-padding-8;
border-left: 1px solid $gl-text-color-secondary;
}
}
- .project-description {
+ .home-panel-description {
@include media-breakpoint-up(md) {
font-size: $gl-font-size-large;
}
@@ -292,12 +232,11 @@
}
}
-.nav > .project-repo-buttons {
+.nav > .project-buttons {
margin-top: 0;
}
-.project-repo-buttons,
-.group-buttons {
+.project-repo-buttons {
.btn {
&:last-child {
margin-left: 0;
@@ -318,8 +257,30 @@
margin-left: 0;
}
}
+
+ .notifications-icon {
+ top: 1px;
+ margin-right: 0;
+ }
+ }
+
+ .icon {
+ top: 0;
+ }
+
+ .count-badge,
+ .btn-xs {
+ height: 24px;
+ }
+
+ .dropdown-toggle,
+ .clone-dropdown-btn {
+ .fa {
+ color: unset;
+ }
}
+ .home-panel-action-button,
.project-action-button {
margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top;
@@ -385,31 +346,6 @@
}
}
-.project-repo-buttons {
- .icon {
- top: 0;
- }
-
- .count-badge,
- .btn-xs {
- height: 24px;
- }
-
- .dropdown-toggle,
- .clone-dropdown-btn {
- .fa {
- color: unset;
- }
- }
-
- .btn {
- .notifications-icon {
- top: 1px;
- margin-right: 0;
- }
- }
-}
-
.split-one {
display: inline-table;
margin-right: 12px;
@@ -459,7 +395,7 @@
margin-right: $gl-padding-4;
margin-bottom: $gl-padding-4;
color: $gl-text-color-secondary;
- background-color: $theme-gray-100;
+ background-color: $gray-100;
line-height: $gl-btn-line-height;
&:hover {
@@ -571,12 +507,6 @@
}
.template-option {
- .logo {
- .btn-template-icon {
- width: 40px !important;
- }
- }
-
padding: 16px 0;
&:not(:first-child) {
@@ -615,9 +545,8 @@
}
.selected-icon {
- svg {
+ img {
display: none;
- top: 7px;
height: 20px;
width: 20px;
}
@@ -642,9 +571,7 @@
.import-buttons {
padding-left: 0;
- display: -webkit-flex;
display: flex;
- -webkit-flex-wrap: wrap;
flex-wrap: wrap;
.btn {
@@ -772,14 +699,11 @@
.project-stats,
.project-buttons {
- font-size: 0;
- text-align: center;
-
.scrolling-tabs-container {
.scrolling-tabs {
margin-top: $gl-padding-8;
- margin-bottom: $gl-padding-8 - $browserScrollbarSize;
- padding-bottom: $browserScrollbarSize;
+ margin-bottom: $gl-padding-8 - $browser-scrollbar-size;
+ padding-bottom: $browser-scrollbar-size;
flex-wrap: wrap;
border-bottom: 0;
}
@@ -787,7 +711,7 @@
.fade-left,
.fade-right {
top: 0;
- height: calc(100% - #{$browserScrollbarSize});
+ height: calc(100% - #{$browser-scrollbar-size});
.fa {
top: 50%;
@@ -914,7 +838,7 @@
}
.repository-language-bar-tooltip-share {
- color: $theme-gray-400;
+ color: $gray-400;
}
pre.light-well {
@@ -973,7 +897,7 @@ pre.light-well {
padding: $gl-padding 0;
@include media-breakpoint-up(lg) {
- padding: $gl-padding-24 0;
+ padding: $gl-padding 0;
}
&.no-description {
@@ -990,7 +914,7 @@ pre.light-well {
}
h2 {
- font-size: $gl-font-size-medium;
+ font-size: $gl-font-size-large;
font-weight: $gl-font-weight-bold;
margin-bottom: 0;
@@ -1013,6 +937,11 @@ pre.light-well {
.flex-wrapper {
min-width: 0;
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
+ flex: 1 1 100%;
+
+ .project-title {
+ line-height: 20px;
+ }
}
p,
@@ -1025,8 +954,10 @@ pre.light-well {
margin: 0;
}
- @include media-breakpoint-up(md) {
- .description {
+ .description {
+ line-height: 1.5;
+
+ @include media-breakpoint-up(md) {
color: $gl-text-color;
}
}
@@ -1049,14 +980,16 @@ pre.light-well {
}
.controls {
- margin-top: $gl-padding;
+ @include media-breakpoint-down(xs) {
+ margin-top: $gl-padding-8;
+ }
- @include media-breakpoint-down(md) {
+ @include media-breakpoint-up(sm) {
margin-top: 0;
}
- @include media-breakpoint-down(xs) {
- margin-top: $gl-padding-8;
+ @include media-breakpoint-up(lg) {
+ flex: 1 1 40%;
}
.icon-wrapper {
@@ -1106,7 +1039,7 @@ pre.light-well {
min-height: 40px;
min-width: 40px;
- .identicon.s64 {
+ .identicon.s48 {
font-size: 16px;
}
}
diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss
index ecd51aa06a4..f7619ccbd20 100644
--- a/app/assets/stylesheets/pages/reports.scss
+++ b/app/assets/stylesheets/pages/reports.scss
@@ -96,7 +96,7 @@
}
&.neutral svg {
- color: $theme-gray-700;
+ color: $gray-700;
}
.ci-status-icon {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 149c3254d84..20bdc6596e9 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -84,9 +84,7 @@ input[type='checkbox']:hover {
.search-icon {
transition: color $default-transition-duration;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
+ user-select: none;
}
.clear-icon {
@@ -185,13 +183,11 @@ input[type='checkbox']:hover {
.search-holder {
@include media-breakpoint-up(sm) {
- display: -webkit-flex;
display: flex;
}
.search-field-holder,
.project-filter-form {
- -webkit-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
margin-right: 0;
diff --git a/app/assets/stylesheets/pages/serverless.scss b/app/assets/stylesheets/pages/serverless.scss
new file mode 100644
index 00000000000..a5b73492380
--- /dev/null
+++ b/app/assets/stylesheets/pages/serverless.scss
@@ -0,0 +1,3 @@
+.url-text-field {
+ cursor: text;
+}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index ccfa4e00a5b..4f9d96da4bd 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -167,12 +167,14 @@
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
+ vertical-align: top;
}
.option-description,
.option-disabled-reason {
margin-left: 30px;
color: $project-option-descr-color;
+ margin-top: -5px;
}
.option-disabled-reason {
@@ -297,7 +299,7 @@
.btn-clipboard {
background-color: $white-light;
- border: 1px solid $theme-gray-200;
+ border: 1px solid $gray-200;
}
.deploy-token-help-block {
@@ -314,8 +316,4 @@
.push-pull-table {
margin-top: 1em;
-
- .mirror-action-buttons {
- padding-right: 0;
- }
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 7d59dd3b5d1..f4d568d02ac 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -33,7 +33,7 @@
border-color: $gl-text-color;
&:not(span):hover {
- background-color: rgba($gl-text-color, .07);
+ background-color: rgba($gl-text-color, 0.07);
}
}
@@ -54,7 +54,7 @@
border-color: $gl-text-color-secondary;
&:not(span):hover {
- background-color: rgba($gl-text-color-secondary, .07);
+ background-color: rgba($gl-text-color-secondary, 0.07);
}
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 3fc37e20c36..2a1e8345755 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -6,9 +6,7 @@
.todos-list > .todo {
// workaround because we cannot use border-colapse
border-top: 1px solid transparent;
- display: -webkit-flex;
display: flex;
- -webkit-flex-direction: row;
flex-direction: row;
&:hover {
@@ -29,23 +27,18 @@
.todo-avatar,
.todo-actions {
@include transition(opacity);
- -webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.todo-actions {
- display: -webkit-flex;
display: flex;
- -webkit-justify-content: center;
justify-content: center;
- -webkit-flex-direction: column;
flex-direction: column;
margin-left: 10px;
min-width: 55px;
}
.todo-item {
- -webkit-flex: 0 1 100%;
flex: 0 1 100%;
min-width: 0;
}
@@ -60,13 +53,13 @@
.todo-avatar,
.todo-item {
- opacity: .6;
+ opacity: 0.6;
}
}
.todo-avatar,
.todo-item {
- opacity: .2;
+ opacity: 0.2;
}
.btn {
@@ -82,7 +75,6 @@
display: flex;
> .title-item {
- -webkit-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0 2px;
@@ -96,7 +88,6 @@
}
.todo-label {
- -webkit-flex: 0 1 auto;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
@@ -222,23 +213,19 @@
}
.todos-empty {
- display: -webkit-flex;
display: flex;
- -webkit-flex-direction: column;
flex-direction: column;
max-width: 900px;
margin-left: auto;
margin-right: auto;
@include media-breakpoint-up(sm) {
- -webkit-flex-direction: row;
flex-direction: row;
padding-top: 80px;
}
}
.todos-empty-content {
- -webkit-align-self: center;
align-self: center;
max-width: 480px;
margin-right: 20px;
@@ -252,7 +239,6 @@
@include media-breakpoint-up(sm) {
width: 300px;
margin-right: 0;
- -webkit-order: 2;
order: 2;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index a46b8679a42..5664f46484e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -172,26 +172,6 @@
text-decoration: inherit;
}
}
-
- .tree_commit {
- max-width: 320px;
-
- .str-truncated {
- max-width: 100%;
- }
- }
-
- .tree_time_ago {
- min-width: 135px;
- }
- }
-
- .tree_author {
- padding-right: 8px;
-
- .commit-author-name {
- color: $gl-text-color;
- }
}
.tree-truncated-warning {
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 84c617c7ec0..7744fd814d0 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -10,7 +10,7 @@
margin-bottom: 15px;
&::before {
- content: "Example";
+ content: 'Example';
color: $ui-dev-kit-example-color;
}
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index e3226c86b0b..189fee98aa0 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -74,6 +74,11 @@ class Admin::AppearancesController < Admin::ApplicationController
favicon_cache
new_project_guidelines
updated_by
+ header_message
+ footer_message
+ message_background_color
+ message_font_color
+ email_header_and_footer_enabled
]
end
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8f683ca06ad..8f267eccc8a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -77,7 +77,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def reset_health_check_token
@application_setting.reset_health_check_access_token!
flash[:notice] = 'New health check access token has been generated!'
- redirect_to :back
+ redirect_back_or_default
end
def clear_repository_check_states
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
index 57f7d3e3951..89d4c4f18d9 100644
--- a/app/controllers/admin/requests_profiles_controller.rb
+++ b/app/controllers/admin/requests_profiles_controller.rb
@@ -11,7 +11,7 @@ class Admin::RequestsProfilesController < Admin::ApplicationController
profile = Gitlab::RequestProfiler::Profile.find(clean_name)
if profile
- render html: profile.content
+ render html: profile.content.html_safe
else
redirect_to admin_requests_profiles_path, alert: 'Profile not found'
end
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 0b6ff491c66..8a00408001e 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::RunnersController < Admin::ApplicationController
- before_action :runner, except: :index
+ before_action :runner, except: [:index, :tag_list]
def index
finder = Admin::RunnersFinder.new(params: params)
@@ -48,6 +48,12 @@ class Admin::RunnersController < Admin::ApplicationController
end
end
+ def tag_list
+ tags = Autocomplete::ActsAsTaggableOn::TagsFinder.new(params: params).execute
+
+ render json: ActsAsTaggableOn::TagSerializer.new.represent(tags)
+ end
+
private
def runner
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index e93be1c1ba2..bfa7c7d0109 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
class Admin::UsersController < Admin::ApplicationController
+ include RoutableActions
+
before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
def index
- @users = User.order_name_asc.filter(params[:filter])
+ @users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page])
@@ -177,11 +179,13 @@ class Admin::UsersController < Admin::ApplicationController
user == current_user
end
- # rubocop: disable CodeReuse/ActiveRecord
def user
- @user ||= User.find_by!(username: params[:id])
+ @user ||= find_routable!(User, params[:id])
+ end
+
+ def build_canonical_path(user)
+ url_for(safe_params.merge(id: user.to_param))
end
- # rubocop: enable CodeReuse/ActiveRecord
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 140a625d333..b7eb6af6d67 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -43,7 +43,10 @@ class ApplicationController < ActionController::Base
:git_import_enabled?, :gitlab_project_import_enabled?,
:manifest_import_enabled?
+ # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
+ # concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze
+ DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache".freeze
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -76,7 +79,7 @@ class ApplicationController < ActionController::Base
end
def redirect_back_or_default(default: root_path, options: {})
- redirect_to request.referer.present? ? :back : default, options
+ redirect_back(fallback_location: default, **options)
end
def not_found
@@ -137,6 +140,8 @@ class ApplicationController < ActionController::Base
if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze
payload[:response] = response.body
end
+
+ payload[:queue_duration] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
end
##
@@ -177,11 +182,17 @@ class ApplicationController < ActionController::Base
# hide existence of the resource, rather tell them they cannot access it using
# the provided message
status ||= message.present? ? :forbidden : :not_found
+ template =
+ if status == :not_found
+ "errors/not_found"
+ else
+ "errors/access_denied"
+ end
respond_to do |format|
format.any { head status }
format.html do
- render "errors/access_denied",
+ render template,
layout: "errors",
status: status,
locals: { message: message }
@@ -227,9 +238,9 @@ class ApplicationController < ActionController::Base
end
def no_cache_headers
- 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"
+ headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE
+ headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility
+ headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
end
def default_headers
@@ -239,10 +250,16 @@ class ApplicationController < ActionController::Base
headers['X-Content-Type-Options'] = 'nosniff'
if current_user
- # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
- # concerns due to caching private data.
- headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL
- headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility
+ headers['Cache-Control'] = default_cache_control
+ headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility
+ end
+ end
+
+ def default_cache_control
+ if request.xhr?
+ ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL
+ else
+ DEFAULT_GITLAB_CACHE_CONTROL
end
end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index b9717b97640..68a2a83f0de 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -24,7 +24,7 @@ class Clusters::ClustersController < Clusters::BaseController
# Note: We are paginating through an array here but this should OK as:
#
# In CE, we can have a maximum group nesting depth of 21, so including
- # project cluster, we can have max 22 clusters for a group hierachy.
+ # project cluster, we can have max 22 clusters for a group hierarchy.
# In EE (Premium) we can have any number, as multiple clusters are
# supported, but the number of clusters are fairly low currently.
#
@@ -127,6 +127,7 @@ class Clusters::ClustersController < Clusters::BaseController
params.require(:cluster).permit(
:enabled,
:environment_scope,
+ :base_domain,
platform_kubernetes_attributes: [
:namespace
]
@@ -136,6 +137,7 @@ class Clusters::ClustersController < Clusters::BaseController
:enabled,
:name,
:environment_scope,
+ :base_domain,
platform_kubernetes_attributes: [
:api_url,
:token,
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index 3cdf4ddf8bb..8b191c86397 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -34,15 +34,11 @@ module BoardsResponses
end
def authorize_read_list
- ability = board.group_board? ? :read_group : :read_list
-
- authorize_action_for!(board.parent, ability)
+ authorize_action_for!(board, :read_list)
end
def authorize_read_issue
- ability = board.group_board? ? :read_group : :read_issue
-
- authorize_action_for!(board.parent, ability)
+ authorize_action_for!(board, :read_issue)
end
def authorize_update_issue
@@ -57,7 +53,7 @@ module BoardsResponses
end
def authorize_admin_list
- authorize_action_for!(board.parent, :admin_list)
+ authorize_action_for!(board, :admin_list)
end
def authorize_action_for!(resource, ability)
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index f0e6adf4dec..54c0510497f 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -6,7 +6,7 @@ module ContinueParams
def continue_params
continue_params = params[:continue]
- return nil unless continue_params
+ return unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
continue_params[:to] = safe_redirect_path(continue_params[:to])
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 3d64ae8b775..cd3fa641e89 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,9 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
+ before_action only: :show do
+ push_frontend_feature_flag(:reply_to_individual_notes)
+ end
end
def permitted_keys
@@ -58,7 +61,8 @@ module IssuableActions
title_text: issuable.title,
description: view_context.markdown_field(issuable, :description),
description_text: issuable.description,
- task_status: issuable.task_status
+ task_status: issuable.task_status,
+ lock_version: issuable.lock_version
}
if issuable.edited?
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 789e0dc736e..c529aabf797 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -91,6 +91,7 @@ module IssuableCollections
options = {
scope: params[:scope],
state: params[:state],
+ confidential: Gitlab::Utils.to_boolean(params[:confidential]),
sort: set_sort_order
}
@@ -129,13 +130,13 @@ module IssuableCollections
return sort_param if Gitlab::Database.read_only?
if user_preference[issuable_sorting_field] != sort_param
- user_preference.update_attribute(issuable_sorting_field, sort_param)
+ user_preference.update(issuable_sorting_field => sort_param)
end
sort_param
end
- # Implement default_sorting_field method on controllers
+ # Implement issuable_sorting_field method on controllers
# to choose which column to store the sorting parameter.
def issuable_sorting_field
nil
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
new file mode 100644
index 00000000000..18ed4027eac
--- /dev/null
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module IssuableCollectionsAction
+ extend ActiveSupport::Concern
+ include IssuableCollections
+ include IssuesCalendar
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def issues
+ @issues = issuables_collection
+ .non_archived
+ .page(params[:page])
+
+ @issuable_meta_data = issuable_meta_data(@issues, collection_type)
+
+ respond_to do |format|
+ format.html
+ format.atom { render layout: 'xml.atom' }
+ end
+ end
+
+ def merge_requests
+ @merge_requests = issuables_collection.page(params[:page])
+
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def issues_calendar
+ render_issues_calendar(issuables_collection)
+ end
+
+ private
+
+ def issuable_sorting_field
+ case action_name
+ when 'issues'
+ Issue::SORTING_PREFERENCE_FIELD
+ when 'merge_requests'
+ MergeRequest::SORTING_PREFERENCE_FIELD
+ else
+ nil
+ end
+ end
+
+ def finder_type
+ case action_name
+ when 'issues', 'issues_calendar'
+ IssuesFinder
+ when 'merge_requests'
+ MergeRequestsFinder
+ else
+ nil
+ end
+ end
+
+ def finder_options
+ super.merge(non_archived: true)
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
deleted file mode 100644
index a75590457d6..00000000000
--- a/app/controllers/concerns/issues_action.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module IssuesAction
- extend ActiveSupport::Concern
- include IssuableCollections
- include IssuesCalendar
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def issues
- @issues = issuables_collection
- .non_archived
- .page(params[:page])
-
- @issuable_meta_data = issuable_meta_data(@issues, collection_type)
-
- respond_to do |format|
- format.html
- format.atom { render layout: 'xml.atom' }
- end
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-
- def issues_calendar
- render_issues_calendar(issuables_collection)
- end
-
- private
-
- def finder_type
- (super if defined?(super)) ||
- (IssuesFinder if %w(issues issues_calendar).include?(action_name))
- end
-end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 5572c3cee2d..57e444319e0 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -123,7 +123,7 @@ module LfsRequest
(authentication_abilities || []).include?(capability)
end
- # Overriden in EE
+ # Overridden in EE
def limit_exceeded?
false
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index ca713192c9e..6402e01ddc0 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -35,7 +35,9 @@ module MembershipActions
respond_to do |format|
format.html do
- message = "User was successfully removed from #{source_type}."
+ source = source_type == 'group' ? 'group and any subresources' : source_type
+
+ message = "User was successfully removed from #{source}."
redirect_to members_page_url, notice: message
end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
deleted file mode 100644
index ed10f32512e..00000000000
--- a/app/controllers/concerns/merge_requests_action.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequestsAction
- extend ActiveSupport::Concern
- include IssuableCollections
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def merge_requests
- @merge_requests = issuables_collection.page(params[:page])
-
- @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-
- private
-
- def finder_type
- (super if defined?(super)) ||
- (MergeRequestsFinder if action_name == 'merge_requests')
- end
-
- def finder_options
- super.merge(non_archived: true)
- end
-end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index eccbe35577b..c0c0160a827 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -8,7 +8,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
- merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: true
})
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0319948a12f..b4fee93713b 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -6,7 +6,6 @@ module NotesActions
extend ActiveSupport::Concern
included do
- prepend_before_action :normalize_create_params, only: [:create]
before_action :set_polling_interval_header, only: [:index]
before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
@@ -44,12 +43,7 @@ module NotesActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create
- create_params = note_params.merge(
- merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
- in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
- )
-
- @note = Notes::CreateService.new(note_project, current_user, create_params).execute
+ @note = Notes::CreateService.new(note_project, current_user, create_note_params).execute
respond_to do |format|
format.json do
@@ -78,7 +72,7 @@ module NotesActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def update
- @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
+ @note = Notes::UpdateService.new(project, current_user, update_note_params).execute(note)
prepare_notes_for_rendering([@note])
respond_to do |format|
@@ -196,24 +190,36 @@ module NotesActions
return access_denied! unless can?(current_user, :admin_note, note)
end
- def note_params
+ def create_note_params
params.require(:note).permit(
- :project_id,
- :noteable_type,
- :noteable_id,
- :commit_id,
- :noteable,
:type,
-
:note,
- :attachment,
+ :line_code, # LegacyDiffNote
+ :position # DiffNote
+ ).tap do |create_params|
+ create_params.merge!(
+ params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id)
+ )
- # LegacyDiffNote
- :line_code,
+ # These params are also sent by the client but we need to set these based on
+ # target_type and target_id because we're checking permissions based on that
+ create_params[:noteable_type] = params[:target_type].classify
+
+ case params[:target_type]
+ when 'commit'
+ create_params[:commit_id] = params[:target_id]
+ when 'merge_request'
+ create_params[:noteable_id] = params[:target_id]
+ # Notes on MergeRequest can have an extra `commit_id` context
+ create_params[:commit_id] = params.dig(:note, :commit_id)
+ else
+ create_params[:noteable_id] = params[:target_id]
+ end
+ end
+ end
- # DiffNote
- :position
- )
+ def update_note_params
+ params.require(:note).permit(:note)
end
def set_polling_interval_header
@@ -248,15 +254,6 @@ module NotesActions
DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity)
end
- # Avoids checking permissions in the wrong object - this ensures that the object we checked permissions for
- # is the object we're actually creating a note in.
- def normalize_create_params
- params[:note].try do |note|
- note[:noteable_id] = params[:target_id]
- note[:noteable_type] = params[:target_type].classify
- end
- end
-
def note_project
strong_memoize(:note_project) do
next nil unless project
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 4b0f0b8255c..f72d25fc54c 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -16,8 +16,6 @@ module PreviewMarkdown
else {}
end
- markdown_params[:markdown_engine] = result[:markdown_engine]
-
render json: {
body: view_context.markdown(result[:text], markdown_params),
references: {
diff --git a/app/controllers/concerns/record_user_last_activity.rb b/app/controllers/concerns/record_user_last_activity.rb
new file mode 100644
index 00000000000..372c803278d
--- /dev/null
+++ b/app/controllers/concerns/record_user_last_activity.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# == RecordUserLastActivity
+#
+# Controller concern that updates the `last_activity_on` field of `users`
+# for any authenticated GET request. The DB update will only happen once per day.
+#
+# In order to determine if you should include this concern or not, please check the
+# description and discussion on this issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/54947
+module RecordUserLastActivity
+ include CookiesHelper
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_user_last_activity
+ end
+
+ def set_user_last_activity
+ return unless request.get?
+ return unless Feature.enabled?(:set_user_last_activity, default_enabled: true)
+ return if Gitlab::Database.read_only?
+
+ if current_user && current_user.last_activity_on != Date.today
+ Users::ActivityService.new(current_user, "visited #{request.path}").execute
+ end
+ end
+end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index ce36da6b715..18015b1de88 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -16,7 +16,7 @@ module RendersNotes
private
def preload_max_access_for_authors(notes, project)
- return nil unless project
+ return unless project
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 515a9eede8e..28e4cece548 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -3,16 +3,19 @@
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
if attachment
+ response_disposition = ::Gitlab::ContentDisposition.format(disposition: disposition, filename: attachment)
+
# Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for
# this to work. However, this override works with AWS.
- redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}",
+ redirect_params[:query] = { "response-content-disposition" => response_disposition,
"response-content-type" => guess_content_type(attachment) }
# By default, Rails will send uploads with an extension of .js with a
# content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection.
send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js'
- send_params.merge!(filename: attachment, disposition: disposition)
+
+ send_params.merge!(filename: attachment, disposition: utf8_encoded_disposition(disposition, attachment))
end
if file_upload.file_storage?
@@ -25,6 +28,18 @@ module SendFileUpload
end
end
+ # Since Rails 5 doesn't properly support support non-ASCII filenames,
+ # we have to add our own to ensure RFC 5987 compliance. However, Rails
+ # 5 automatically appends `filename#{filename}` here:
+ # https://github.com/rails/rails/blob/v5.0.7/actionpack/lib/action_controller/metal/data_streaming.rb#L137
+ # Rails 6 will have https://github.com/rails/rails/pull/33829, so we
+ # can get rid of this special case handling when we upgrade.
+ def utf8_encoded_disposition(disposition, filename)
+ content = ::Gitlab::ContentDisposition.new(disposition: disposition, filename: filename)
+
+ "#{disposition}; #{content.utf8_filename}"
+ end
+
def guess_content_type(filename)
types = MIME::Types.type_for(filename)
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c6ae4fe15bf..48451bedcc2 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -72,7 +72,7 @@ module ServiceParams
dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
service_params = params.permit(:id, service: allowed_service_params + dynamic_params)
- if service_params[:service].is_a?(Hash)
+ if service_params[:service].is_a?(ActionController::Parameters)
FILTER_BLANK_PARAMS.each do |param|
service_params[:service].delete(param) if service_params[:service][param].blank?
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 0eea0cdd50f..4ec0e94df9a 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -7,12 +7,12 @@ module UploadsActions
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
def create
- link_to_file = UploadService.new(model, params[:file], uploader_class).execute
+ uploader = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
- if link_to_file
+ if uploader
format.json do
- render json: { link: link_to_file }
+ render json: { link: uploader.to_h }
end
else
format.json do
@@ -29,7 +29,13 @@ module UploadsActions
def show
return render_404 unless uploader&.exists?
- expires_in 0.seconds, must_revalidate: true, private: true
+ if cache_publicly?
+ # We need to reset caching from the applications controller to get rid of the no-store value
+ headers['Cache-Control'] = ''
+ expires_in 5.minutes, public: true, must_revalidate: false
+ else
+ expires_in 0.seconds, must_revalidate: true, private: true
+ end
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
@@ -114,6 +120,10 @@ module UploadsActions
nil
end
+ def cache_publicly?
+ false
+ end
+
def model
strong_memoize(:model) { find_model }
end
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index cee0753a021..0e9fdc60363 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
+ include RecordUserLastActivity
layout 'dashboard'
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 3802aa5f40f..912036da0ea 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -25,9 +25,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def group_milestones
- groups = GroupsFinder.new(current_user, all_available: false).execute
-
- DashboardGroupMilestone.build_collection(groups)
+ DashboardGroupMilestone.build_collection(groups, params)
end
# See [#39545](https://gitlab.com/gitlab-org/gitlab-ce/issues/39545) for info about the deprecation of dynamic milestones
@@ -45,6 +43,6 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
end
def groups
- @groups ||= GroupsFinder.new(current_user, state_all: true).execute
+ @groups ||= GroupsFinder.new(current_user, all_available: false).execute
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index f073b6de444..b044affd4e8 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -13,7 +13,13 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true))
respond_to do |format|
- format.html
+ format.html do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
+ # Also https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
+ end
format.atom do
load_events
render layout: 'xml.atom'
@@ -53,6 +59,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def load_projects(finder_params)
+ @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute
+ @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute
+
projects = ProjectsFinder
.new(params: finder_params, current_user: current_user)
.execute
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index be2d9512c01..75329b05a6f 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
class DashboardController < Dashboard::ApplicationController
- include IssuesAction
- include MergeRequestsAction
+ include IssuableCollectionsAction
prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 778fdda8dbd..f3d76c5a478 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -15,7 +15,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
@@ -30,7 +30,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
@@ -44,7 +44,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
@@ -55,6 +55,9 @@ class Explore::ProjectsController < Explore::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def load_projects
+ @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute
+ @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute
+
projects = ProjectsFinder.new(current_user: current_user, params: params)
.execute
.includes(:route, :creator, :group, namespace: [:route, :owner])
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
index dd9f5af61b3..ed0995e7ffd 100644
--- a/app/controllers/google_api/authorizations_controller.rb
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -2,6 +2,10 @@
module GoogleApi
class AuthorizationsController < ApplicationController
+ include Gitlab::Utils::StrongMemoize
+
+ before_action :validate_session_key!
+
def callback
token, expires_at = GoogleApi::CloudPlatform::Client
.new(nil, callback_google_api_auth_url)
@@ -11,21 +15,27 @@ module GoogleApi
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] =
expires_at.to_s
- state_redirect_uri = redirect_uri_from_session_key(params[:state])
-
- if state_redirect_uri
- redirect_to state_redirect_uri
- else
- redirect_to root_path
- end
+ redirect_to redirect_uri_from_session
end
private
- def redirect_uri_from_session_key(state)
- key = GoogleApi::CloudPlatform::Client
- .session_key_for_redirect_uri(params[:state])
- session[key] if key
+ def validate_session_key!
+ access_denied! unless redirect_uri_from_session.present?
+ end
+
+ def redirect_uri_from_session
+ strong_memoize(:redirect_uri_from_session) do
+ if params[:state].present?
+ session[session_key_for_redirect_uri(params[:state])]
+ else
+ nil
+ end
+ end
+ end
+
+ def session_key_for_redirect_uri(state)
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(state)
end
end
end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 3ef03bc9622..e147d32be2e 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -3,9 +3,16 @@
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
- prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
+
+ # Allow missing CSRF tokens, this would mean that if a CSRF is invalid or missing,
+ # the user won't be authenticated but can proceed as an anonymous user.
+ #
+ # If a CSRF is valid, the user is authenticated. This makes it easier to play
+ # around in GraphiQL.
+ protect_from_forgery with: :null_session, only: :execute
before_action :check_graphql_feature_flag!
+ before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
def execute
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index cdc6f53df8e..51fdb6c05fb 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -2,6 +2,7 @@
class Groups::BoardsController < Groups::ApplicationController
include BoardsResponses
+ include RecordUserLastActivity
before_action :assign_endpoint_vars
before_action :boards, only: :index
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
index d549f793ad7..236a19a8dc4 100644
--- a/app/controllers/groups/children_controller.rb
+++ b/app/controllers/groups/children_controller.rb
@@ -35,7 +35,7 @@ module Groups
def setup_children(parent)
@children = GroupDescendantsFinder.new(current_user: current_user,
parent_group: parent,
- params: params).execute
+ params: params.to_unsafe_h).execute
@children = @children.page(params[:page])
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index b42116b0f36..7ed4384089b 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -43,14 +43,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
- if @milestone.legacy_group_milestone?
- update_params = milestone_params.select { |key| key == "state_event" }
- milestones = @milestone.milestones
- else
- update_params = milestone_params
- milestones = [@milestone]
- end
-
+ milestones, update_params = get_milestones_for_update
milestones.each do |milestone|
Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
end
@@ -71,6 +64,14 @@ class Groups::MilestonesController < Groups::ApplicationController
private
+ def get_milestones_for_update
+ if @milestone.legacy_group_milestone?
+ [@milestone.milestones, legacy_milestone_params]
+ else
+ [[@milestone], milestone_params]
+ end
+ end
+
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestone, group)
end
@@ -79,6 +80,10 @@ class Groups::MilestonesController < Groups::ApplicationController
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
+ def legacy_milestone_params
+ params.require(:milestone).permit(:state_event)
+ end
+
def milestone_path
if @milestone.legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
@@ -110,6 +115,6 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def search_params
- params.permit(:state).merge(group_ids: group.id)
+ params.permit(:state, :search_title).merge(group_ids: group.id)
end
end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index c1dcc463de7..f476f428fdb 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -4,7 +4,7 @@ module Groups
module Settings
class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
- before_action :authorize_admin_pipeline!
+ before_action :authorize_admin_group!
def show
define_ci_variables
@@ -26,8 +26,8 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
- def authorize_admin_pipeline!
- return render_404 unless can?(current_user, :admin_pipeline, group)
+ def authorize_admin_group!
+ return render_404 unless can?(current_user, :admin_group, group)
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c5d8ac2ed77..4e50106398a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -2,10 +2,10 @@
class GroupsController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
- include IssuesAction
- include MergeRequestsAction
+ include IssuableCollectionsAction
include ParamsBackwardCompatibility
include PreviewMarkdown
+ include RecordUserLastActivity
respond_to :html
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index e5a1fc9d6ff..a9d6addd4a4 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -13,9 +13,10 @@ class HelpController < ApplicationController
# Remove YAML frontmatter so that it doesn't look weird
@help_index = File.read(Rails.root.join('doc', 'README.md')).sub(YAML_FRONT_MATTER_REGEXP, '')
- # Prefix Markdown links with `help/` unless they are external links
- # See http://rubular.com/r/X3baHTbPO2
- @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do
+ # Prefix Markdown links with `help/` unless they are external links.
+ # '//' not necessarily part of URL, e.g., mailto:mail@example.com
+ # See https://rubular.com/r/DFHZl5w8d3bpzV
+ @help_index.gsub!(%r{(?<delim>\]\()(?!\w+:)(?!/)(?<link>[^\)\(]+\))}) do
"#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}"
end
end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 042b6b1264f..9b45be6db99 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -18,6 +18,7 @@ class Import::BaseController < ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ # deprecated: being replaced by app/services/import/base_service.rb
def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names
@@ -32,6 +33,7 @@ class Import::BaseController < ApplicationController
current_user.namespace
end
+ # deprecated: being replaced by app/services/import/base_service.rb
def project_save_error(project)
project.errors.full_messages.join(', ')
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 1b30b4dda36..2b1395f364f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
+ response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
@@ -89,7 +89,7 @@ class Import::BitbucketController < Import::BaseController
end
def go_to_bitbucket_for_permissions
- redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end
def bitbucket_unauthorized
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 575c40d5f6f..f333e43b892 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -13,7 +13,10 @@ class Import::BitbucketServerController < Import::BaseController
# Repository names are limited to 128 characters. They must start with a
# letter or number and may contain spaces, hyphens, underscores, and periods.
# (https://community.atlassian.com/t5/Answers-Developer-Questions/stash-repository-names/qaq-p/499054)
- VALID_BITBUCKET_CHARS = /\A[\w\-_\.\s]+\z/
+ #
+ # Bitbucket Server starts personal project names with a tilde.
+ VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/
+ VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/
def new
end
@@ -40,7 +43,7 @@ class Import::BitbucketServerController < Import::BaseController
else
render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
end
- rescue BitbucketServer::Client::ServerError => e
+ rescue BitbucketServer::Connection::ConnectionError => e
render json: { errors: "Unable to connect to server: #{e}" }, status: :unprocessable_entity
end
@@ -62,7 +65,7 @@ class Import::BitbucketServerController < Import::BaseController
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
- rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e
+ rescue BitbucketServer::Connection::ConnectionError => e
flash[:alert] = "Unable to connect to server: #{e}"
clear_session_data
redirect_to new_import_bitbucket_server_path
@@ -91,7 +94,7 @@ class Import::BitbucketServerController < Import::BaseController
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
return render_validation_error('Missing repository slug') unless @repo_slug.present?
- return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_CHARS
+ return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_PROJECT_CHARS
return render_validation_error('Invalid repository slug') unless @repo_slug =~ VALID_BITBUCKET_CHARS
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index f067ef625aa..68ad8650dba 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
class Import::GiteaController < Import::GithubController
+ extend ::Gitlab::Utils::Override
+
def new
- if session[access_token_key].present? && session[host_key].present?
+ if session[access_token_key].present? && provider_url.present?
redirect_to status_import_url
end
end
@@ -12,8 +14,8 @@ class Import::GiteaController < Import::GithubController
super
end
+ # Must be defined or it will 404
def status
- @gitea_host_url = session[host_key]
super
end
@@ -23,25 +25,33 @@ class Import::GiteaController < Import::GithubController
:"#{provider}_host_url"
end
- # Overridden methods
+ override :provider
def provider
:gitea
end
+ override :provider_url
+ def provider_url
+ session[host_key]
+ end
+
# Gitea is not yet an OAuth provider
# See https://github.com/go-gitea/gitea/issues/27
+ override :logged_in_with_provider?
def logged_in_with_provider?
false
end
+ override :provider_auth
def provider_auth
- if session[access_token_key].blank? || session[host_key].blank?
+ if session[access_token_key].blank? || provider_url.blank?
redirect_to new_import_gitea_url,
alert: 'You need to specify both an Access Token and a Host URL.'
end
end
+ override :client_options
def client_options
- { host: session[host_key], api_version: 'v1' }
+ { host: provider_url, api_version: 'v1' }
end
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index d4c26fa0709..aa4aa0fbdac 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
class Import::GithubController < Import::BaseController
+ include ImportHelper
+
before_action :verify_import_enabled
- before_action :provider_auth, only: [:status, :jobs, :create]
+ before_action :provider_auth, only: [:status, :realtime_changes, :create]
+ before_action :expire_etag_cache, only: [:status, :create]
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
@@ -24,47 +27,86 @@ class Import::GithubController < Import::BaseController
redirect_to status_import_url
end
- # rubocop: disable CodeReuse/ActiveRecord
def status
- @repos = client.repos
- @already_added_projects = find_already_added_projects(provider)
- already_added_projects_names = @already_added_projects.pluck(:import_source)
-
- @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def jobs
- render json: find_jobs(provider)
+ # Request repos to display error page if provider token is invalid
+ # Improving in https://gitlab.com/gitlab-org/gitlab-ce/issues/55585
+ client_repos
+
+ respond_to do |format|
+ format.json do
+ render json: { imported_projects: serialized_imported_projects,
+ provider_repos: serialized_provider_repos,
+ namespaces: serialized_namespaces }
+ end
+ format.html
+ end
end
def create
- repo = client.repo(params[:repo_id].to_i)
- project_name = params[:new_name].presence || repo.name
- namespace_path = params[:target_namespace].presence || current_user.namespace_path
- target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
-
- if can?(current_user, :create_projects, target_namespace)
- project = Gitlab::LegacyGithubImport::ProjectCreator
- .new(repo, project_name, target_namespace, current_user, access_params, type: provider)
- .execute(extra_project_attrs)
-
- if project.persisted?
- render json: ProjectSerializer.new.represent(project)
- else
- render json: { errors: project_save_error(project) }, status: :unprocessable_entity
- end
+ result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider)
+
+ if result[:status] == :success
+ render json: serialized_imported_projects(result[:project])
else
- render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity
+ render json: { errors: result[:message] }, status: result[:http_status]
end
end
+ def realtime_changes
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ render json: find_jobs(provider)
+ end
+
private
+ def import_params
+ params.permit(permitted_import_params)
+ end
+
+ def permitted_import_params
+ [:repo_id, :new_name, :target_namespace]
+ end
+
+ def serialized_imported_projects(projects = already_added_projects)
+ ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url)
+ end
+
+ def serialized_provider_repos
+ repos = client_repos.reject { |repo| already_added_project_names.include? repo.full_name }
+ ProviderRepoSerializer.new(current_user: current_user).represent(repos, provider: provider, provider_url: provider_url)
+ end
+
+ def serialized_namespaces
+ NamespaceSerializer.new.represent(namespaces)
+ end
+
+ def already_added_projects
+ @already_added_projects ||= find_already_added_projects(provider)
+ end
+
+ def already_added_project_names
+ @already_added_projects_names ||= already_added_projects.pluck(:import_source) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ def namespaces
+ current_user.manageable_groups_with_routes
+ end
+
+ def expire_etag_cache
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(realtime_changes_path)
+ end
+ end
+
def client
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
end
+ def client_repos
+ @client_repos ||= client.repos
+ end
+
def verify_import_enabled
render_404 unless import_enabled?
end
@@ -77,6 +119,10 @@ class Import::GithubController < Import::BaseController
__send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend
end
+ def realtime_changes_path
+ public_send("realtime_changes_import_#{provider}_path", format: :json) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def new_import_url
public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -86,7 +132,7 @@ class Import::GithubController < Import::BaseController
end
def callback_import_url
- public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
+ public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
@@ -108,6 +154,14 @@ class Import::GithubController < Import::BaseController
:github
end
+ def provider_url
+ strong_memoize(:provider_url) do
+ provider = Gitlab::Auth::OAuth::Provider.config_for('github')
+
+ provider&.dig('url').presence || 'https://github.com'
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def logged_in_with_provider?
current_user.identities.exists?(provider: provider)
@@ -124,10 +178,6 @@ class Import::GithubController < Import::BaseController
{}
end
- def extra_project_attrs
- {}
- end
-
def extra_import_params
{}
end
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 384f308269a..43c4f4d220e 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -17,7 +17,8 @@ class NotificationSettingsController < ApplicationController
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
if params[:hide_label].present?
- render_response("projects/buttons/_notifications")
+ btn_class = params[:project_id].present? ? 'btn-xs' : ''
+ render_response("shared/notifications/_new_button", btn_class)
else
render_response
end
@@ -41,9 +42,9 @@ class NotificationSettingsController < ApplicationController
can?(current_user, ability_name, resource)
end
- def render_response(response_template = "shared/notifications/_button")
+ def render_response(response_template = "shared/notifications/_button", btn_class = nil)
render json: {
- html: view_to_html_string(response_template, notification_setting: @notification_setting),
+ html: view_to_html_string(response_template, notification_setting: @notification_setting, btn_class: btn_class),
saved: @saved
}
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 30be50d4595..cc2bb99f55b 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -4,7 +4,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
- protect_from_forgery except: [:kerberos, :saml, :cas3], prepend: true
+ protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
def handle_omniauth
omniauth_flow(Gitlab::Auth::OAuth)
@@ -75,6 +75,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
def omniauth_flow(auth_module, identity_linker: nil)
+ if fragment = request.env.dig('omniauth.params', 'redirect_fragment').presence
+ store_redirect_fragment(fragment)
+ end
+
if current_user
log_audit_event(current_user, with: oauth['provider'])
@@ -112,8 +116,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
+ def build_auth_user(auth_user_class)
+ auth_user_class.new(oauth)
+ end
+
def sign_in_user_flow(auth_user_class)
- auth_user = auth_user_class.new(oauth)
+ auth_user = build_auth_user(auth_user_class)
user = auth_user.find_and_update!
if auth_user.valid_sign_in?
@@ -189,4 +197,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present?
end
+
+ def store_redirect_fragment(redirect_fragment)
+ key = stored_location_key_for(:user)
+ location = session[key]
+ if uri = parse_uri(location)
+ uri.fragment = redirect_fragment
+ store_location_for(:user, uri.to_s)
+ end
+ end
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 2912a22411e..28f113b5cbe 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -5,7 +5,7 @@ class PasswordsController < Devise::PasswordsController
before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create]
- before_action :throttle_reset, only: [:create]
+ before_action :throttle_reset, only: [:create]
# rubocop: disable CodeReuse/ActiveRecord
def edit
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index efe7ede5efa..c473023cacb 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -2,15 +2,6 @@
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
- @sessions = ActiveSession.list(current_user)
- end
-
- def destroy
- ActiveSession.destroy(current_user, params[:id])
-
- respond_to do |format|
- format.html { redirect_to profile_active_sessions_url, status: :found }
- format.js { head :ok }
- end
+ @sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 37ac11dc6a1..0227af2c266 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -33,12 +33,18 @@ class Profiles::PreferencesController < Profiles::ApplicationController
end
def preferences_params
- params.require(:user).permit(
+ params.require(:user).permit(preferences_param_names)
+ end
+
+ def preferences_param_names
+ [
:color_scheme_id,
:layout,
:dashboard,
:project_view,
- :theme_id
- )
+ :theme_id,
+ :first_day_of_week,
+ :preferred_language
+ ]
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 15248d2d08f..b9c52618d4b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -104,7 +104,6 @@ class ProfilesController < Profiles::ApplicationController
:username,
:website_url,
:organization,
- :preferred_language,
:private_profile,
:include_private_contributions,
status: [:emoji, :message]
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index e0677ce3fbc..6504fd6c08a 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -17,7 +17,7 @@ class Projects::ApplicationController < ApplicationController
def project
return @project if @project
- return nil unless params[:project_id] || params[:id]
+ return unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 1a91e07b97f..2ef18d900f2 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -85,20 +85,15 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def build_from_id
- project.builds.find_by(id: params[:job_id]) if params[:job_id]
+ project.builds.find_by_id(params[:job_id]) if params[:job_id]
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def build_from_ref
return unless @ref_name
- builds = project.latest_successful_builds_for(@ref_name)
- builds.find_by(name: params[:job])
+ project.latest_successful_build_for(params[:job], @ref_name)
end
- # rubocop: enable CodeReuse/ActiveRecord
def artifacts_file
@artifacts_file ||= build&.artifacts_file_for_type(params[:file_type] || :archive)
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 9c130af8394..0e3f13045ce 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Projects::AutocompleteSourcesController < Projects::ApplicationController
+ before_action :authorize_read_milestone!, only: :milestones
+
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index c24bf211760..09a384e89ab 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -21,11 +21,22 @@ class Projects::BadgesController < Projects::ApplicationController
private
+ def badge_layout
+ case params[:style]
+ when 'flat'
+ 'badge'
+ when 'flat-square'
+ 'badge_flat-square'
+ else
+ 'badge'
+ end
+ end
+
def render_badge(badge)
respond_to do |format|
format.html { render_404 }
format.svg do
- render 'badge', locals: { badge: badge.template }
+ render badge_layout, locals: { badge: badge.template }
end
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 60fabd15333..77672e7d9fc 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -93,7 +93,7 @@ class Projects::BlobController < Projects::ApplicationController
@blob.load_all_data!
@lines = @blob.present.highlight.lines
- @form = UnfoldForm.new(params)
+ @form = UnfoldForm.new(params.to_unsafe_h)
@lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe)
@@ -260,7 +260,7 @@ class Projects::BlobController < Projects::ApplicationController
extension: blob.extension,
size: blob.raw_size,
mime_type: blob.mime_type,
- binary: blob.raw_binary?,
+ binary: blob.binary?,
simple_viewer: blob.simple_viewer&.class&.partial_name,
rich_viewer: blob.rich_viewer&.class&.partial_name,
show_viewer_switcher: !!blob.show_viewer_switcher?,
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index a6bfb913900..32b7f3207ef 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -29,7 +29,8 @@ class Projects::BranchesController < Projects::ApplicationController
Gitlab::GitalyClient.allow_n_plus_1_calls do
@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
+ [memo, diverging_commit_counts.values_at(:behind, :ahead, :distance)]
+ .flatten.compact.max
end
end
diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb
index 7d4d566499c..4274c356227 100644
--- a/app/controllers/projects/build_artifacts_controller.rb
+++ b/app/controllers/projects/build_artifacts_controller.rb
@@ -44,18 +44,13 @@ class Projects::BuildArtifactsController < Projects::ApplicationController
@job ||= job_from_id || job_from_ref
end
- # rubocop: disable CodeReuse/ActiveRecord
def job_from_id
- project.builds.find_by(id: params[:build_id]) if params[:build_id]
+ project.builds.find_by_id(params[:build_id]) if params[:build_id]
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def job_from_ref
return unless @ref_name
- jobs = project.latest_successful_builds_for(@ref_name)
- jobs.find_by(name: params[:job])
+ project.latest_successful_build_for(params[:job], @ref_name)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
index 2090af0a111..d7a0b7ece14 100644
--- a/app/controllers/projects/ci/lints_controller.rb
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -8,7 +8,7 @@ class Projects::Ci::LintsController < Projects::ApplicationController
def create
@content = params[:content]
- @error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options)
+ @error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options)
@status = @error.blank?
if @error.blank?
@@ -24,6 +24,10 @@ class Projects::Ci::LintsController < Projects::ApplicationController
private
def yaml_processor_options
- { project: @project, sha: project.repository.commit.sha }
+ {
+ project: @project,
+ user: current_user,
+ sha: project.repository.commit.sha
+ }
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 32fc5140366..939a09d4fd2 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -24,10 +24,10 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
- format.html do
+ format.html do
render
end
- format.diff do
+ format.diff do
send_git_diff(@project.repository, @commit.diff_refs)
end
format.patch do
@@ -65,7 +65,11 @@ class Projects::CommitController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def merge_requests
- @merge_requests = @commit.merge_requests.map do |mr|
+ @merge_requests = MergeRequestsFinder.new(
+ current_user,
+ project_id: @project.id,
+ commit_sha: @commit.sha
+ ).execute.map do |mr|
{ iid: mr.iid, path: merge_request_path(mr), title: mr.title }
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index b62606067c0..028390c7e2a 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -40,7 +40,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
- DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
+ DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
.represent(discussion, context: self, render_truncated_diff_lines: true)
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index a63eea0ca0e..e9cd475a199 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -11,10 +11,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
- before_action do
- push_frontend_feature_flag(:area_chart, project)
- end
-
def index
@environments = project.environments
.with_state(params[:scope] || :available)
@@ -25,11 +21,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .within_folders
- .represent(@environments),
+ environments: serialize_environments(request, response, params[:nested]),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
@@ -37,6 +29,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ # Returns all environments for a given folder
# rubocop: disable CodeReuse/ActiveRecord
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@@ -48,10 +41,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.html
format.json do
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .represent(@environments),
+ environments: serialize_environments(request, response),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
@@ -163,6 +153,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def search
+ respond_to do |format|
+ format.json do
+ environment_names = search_environment_names
+
+ render json: environment_names, status: environment_names.any? ? :ok : :no_content
+ end
+ end
+ end
+
private
def verify_api_request!
@@ -186,6 +186,20 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment ||= project.environments.find(params[:id])
end
+ def search_environment_names
+ return [] unless params[:query]
+
+ project.environments.for_name_like(params[:query]).pluck_names
+ end
+
+ def serialize_environments(request, response, nested = false)
+ EnvironmentSerializer
+ .new(project: @project, current_user: @current_user)
+ .tap { |serializer| serializer.within_folders if nested }
+ .with_pagination(request, response)
+ .represent(@environments)
+ end
+
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
new file mode 100644
index 00000000000..88d0755f41f
--- /dev/null
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+class Projects::ErrorTrackingController < Projects::ApplicationController
+ before_action :authorize_read_sentry_issue!
+
+ POLLING_INTERVAL = 10_000
+
+ def index
+ respond_to do |format|
+ format.html
+ format.json do
+ set_polling_interval
+ render_index_json
+ end
+ end
+ end
+
+ def list_projects
+ respond_to do |format|
+ format.json do
+ render_project_list_json
+ end
+ end
+ end
+
+ private
+
+ def render_index_json
+ service = ErrorTracking::ListIssuesService.new(project, current_user)
+ result = service.execute
+
+ unless result[:status] == :success
+ return render json: { message: result[:message] },
+ status: result[:http_status] || :bad_request
+ end
+
+ render json: {
+ errors: serialize_errors(result[:issues]),
+ external_url: service.external_url
+ }
+ end
+
+ def render_project_list_json
+ service = ErrorTracking::ListProjectsService.new(
+ project,
+ current_user,
+ list_projects_params
+ )
+ result = service.execute
+
+ if result[:status] == :success
+ render json: {
+ projects: serialize_projects(result[:projects])
+ }
+ else
+ return render(
+ status: result[:http_status] || :bad_request,
+ json: {
+ message: result[:message]
+ }
+ )
+ end
+ end
+
+ def list_projects_params
+ params.require(:error_tracking_setting).permit([:api_host, :token])
+ end
+
+ def set_polling_interval
+ Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
+ end
+
+ def serialize_errors(errors)
+ ErrorTracking::ErrorSerializer
+ .new(project: project, user: current_user)
+ .represent(errors)
+ end
+
+ def serialize_projects(projects)
+ ErrorTracking::ProjectSerializer
+ .new(project: project, user: current_user)
+ .represent(projects)
+ end
+end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index c0aa39d87c6..0c5328fc941 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -20,6 +20,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
+ enqueue_fetch_statistics_update
+
render_ok
end
@@ -67,6 +69,13 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :service_unavailable
end
+ def enqueue_fetch_statistics_update
+ return if wiki?
+ return unless project.daily_statistics_enabled?
+
+ ProjectDailyStatisticsWorker.perform_async(project.id)
+ end
+
def access
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
@@ -80,9 +89,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access_check
- # Use the magic string '_any' to indicate we do not know what the
- # changes are. This is also what gitlab-shell does.
- access.check(git_command, '_any')
+ access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 925b6ed9bfd..c80fce513f6 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -45,7 +45,14 @@ class Projects::GraphsController < Projects::ApplicationController
end
def get_languages
- @languages = @project.repository.languages
+ @languages =
+ if @project.repository_languages.present?
+ @project.repository_languages.map do |lang|
+ { value: lang.share, label: lang.name, color: lang.color, highlight: lang.color }
+ end
+ else
+ @project.repository.languages
+ end
end
def fetch_graph
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 7c713c19762..bc942ba9288 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -13,9 +13,10 @@ class Projects::GroupLinksController < Projects::ApplicationController
group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
if group
- return render_404 unless can?(current_user, :read_group, group)
+ result = Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
+ return render_404 if result[:http_status] == 404
- Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
+ flash[:alert] = result[:message] if result[:http_status] == 409
else
flash[:alert] = 'Please select a group.'
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 5ed46fc0545..b9d02a62fc3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -8,9 +8,10 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include IssuesCalendar
include SpammableActions
+ include RecordUserLastActivity
def self.issue_except_actions
- %i[index calendar new create bulk_update]
+ %i[index calendar new create bulk_update import_csv]
end
def self.set_issuables_index_only_actions
@@ -19,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
- prepend_before_action :authenticate_new_issue!, only: [:new]
+ prepend_before_action :authenticate_user!, only: [:new]
prepend_before_action :store_uri, only: [:new, :show]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
@@ -37,6 +38,8 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
+ before_action :authorize_import_issues!, only: [:import_csv]
+
before_action :set_suggested_issues_feature_flags, only: [:new]
respond_to :html
@@ -175,8 +178,24 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
+ def import_csv
+ if uploader = UploadService.new(project, params[:file]).execute
+ ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id)
+
+ flash[:notice] = _("Your issues are being imported. Once finished, you'll get a confirmation email.")
+ else
+ flash[:alert] = _("File upload error.")
+ end
+
+ redirect_to project_issues_path(project)
+ end
+
protected
+ def issuable_sorting_field
+ Issue::SORTING_PREFERENCE_FIELD
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def issue
return @issue if defined?(@issue)
@@ -228,15 +247,7 @@ class Projects::IssuesController < Projects::ApplicationController
task_num
lock_version
discussion_locked
- ] + [{ label_ids: [], assignee_ids: [] }]
- end
-
- def authenticate_new_issue!
- return if current_user
-
- notice = "Please sign in to create the new issue."
-
- redirect_to new_user_session_path, notice: notice
+ ] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
end
def store_uri
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index bfbbcba883f..d5ce790e2d9 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,10 +4,10 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
include ContinueParams
- before_action :build, except: [:index, :cancel_all]
+ before_action :build, except: [:index]
before_action :authorize_read_build!
before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
+ except: [:index, :show, :status, :raw, :trace, :erase]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
@@ -39,16 +39,6 @@ class Projects::JobsController < Projects::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
- def cancel_all
- return access_denied! unless can?(current_user, :update_build, project)
-
- @project.builds.running_or_pending.each do |build|
- build.cancel if can?(current_user, :update_build, build)
- end
-
- redirect_to project_jobs_path(project)
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def show
@pipeline = @build.pipeline
diff --git a/app/controllers/projects/lfs_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb
index fc67cd72faa..6aacb9d9a56 100644
--- a/app/controllers/projects/lfs_locks_api_controller.rb
+++ b/app/controllers/projects/lfs_locks_api_controller.rb
@@ -4,19 +4,19 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController
include LfsRequest
def create
- @result = Lfs::LockFileService.new(project, user, params).execute
+ @result = Lfs::LockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def unlock
- @result = Lfs::UnlockFileService.new(project, user, params).execute
+ @result = Lfs::UnlockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def index
- @result = Lfs::LocksFinderService.new(project, user, params).execute
+ @result = Lfs::LocksFinderService.new(project, user, lfs_params).execute
render_json(@result[:locks])
end
@@ -69,4 +69,8 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController
def upload_request?
%w(create unlock verify).include?(params[:action])
end
+
+ def lfs_params
+ params.permit(:id, :path, :force)
+ end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index babeee48ef3..013e01b82aa 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -5,7 +5,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
include WorkhorseRequest
include SendFileUpload
- skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
+ skip_before_action :verify_workhorse_api!, only: :download
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 368ee89ff5c..6045ee4e171 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -34,13 +34,17 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num,
:title,
:discussion_locked,
- label_ids: []
+ label_ids: [],
+ update_task: [:index, :checked, :line_number, :line_source]
]
end
def set_pipeline_variables
- @pipelines = @merge_request.all_pipelines
- @pipeline = @merge_request.head_pipeline
- @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
+ @pipelines =
+ if can?(current_user, :read_pipeline, @project)
+ @merge_request.all_pipelines
+ else
+ Ci::Pipeline.none
+ end
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 5639402a1e9..32cefe54613 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -89,7 +89,11 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
+
+ # Gitaly N+1 issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/58096
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
+ end
end
def define_new_vars
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index ddffbb17ace..456d2c34768 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
- include DiffForPath
include DiffHelper
include RendersNotes
@@ -47,8 +46,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
# rubocop: disable CodeReuse/ActiveRecord
def commit
- return nil unless commit_id = params[:commit_id].presence
- return nil unless @merge_request.all_commits.exists?(sha: commit_id)
+ return unless commit_id = params[:commit_id].presence
+ return unless @merge_request.all_commits.exists?(sha: commit_id)
@commit ||= @project.commit(commit_id)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8b8eac7ff8e..46a44841c31 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include RendersCommits
include ToggleAwardEmoji
include IssuableCollections
+ include RecordUserLastActivity
skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
@@ -15,6 +16,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
+ before_action only: [:show] do
+ push_frontend_feature_flag(:diff_tree_filtering, default_enabled: true)
+ end
+
def index
@merge_requests = @issuables
@@ -55,7 +60,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: serializer.represent(@merge_request, serializer: params[:serializer])
end
- format.patch do
+ format.patch do
break render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
@@ -218,18 +223,28 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
head :ok
end
+ def discussions
+ merge_request.preload_discussions_diff_highlight
+
+ super
+ end
+
protected
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
+ def issuable_sorting_field
+ MergeRequest::SORTING_PREFERENCE_FIELD
+ end
+
def merge_params
params.permit(merge_params_attributes)
end
def merge_params_attributes
- [:should_remove_source_branch, :commit_message, :squash]
+ [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash]
end
def merge_when_pipeline_succeeds_active?
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 8e68014a30d..f6f61b6e5fb 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -144,9 +144,9 @@ class Projects::MilestonesController < Projects::ApplicationController
def search_params
if request.format.json? && project_group && can?(current_user, :read_group, project_group)
- groups = project_group.self_and_ancestors_ids
+ groups = project_group.self_and_ancestors.select(:id)
end
- params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
+ params.permit(:state, :search_title).merge(project_ids: @project.id, group_ids: groups)
end
end
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index c1ad6707c97..73e629ab7c3 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -5,7 +5,8 @@ class Projects::PagesController < Projects::ApplicationController
before_action :require_pages_enabled!
before_action :authorize_read_pages!, only: [:show]
- before_action :authorize_update_pages!, except: [:show]
+ before_action :authorize_update_pages!, except: [:show, :destroy]
+ before_action :authorize_remove_pages!, only: [:destroy]
# rubocop: disable CodeReuse/ActiveRecord
def show
@@ -18,7 +19,7 @@ class Projects::PagesController < Projects::ApplicationController
project.pages_domains.destroy_all # rubocop: disable DestroyAll
respond_to do |format|
- format.html do
+ format.html do
redirect_to project_pages_path(@project),
status: 302,
notice: 'Pages were removed'
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index 439ec9b1731..58b1bc54181 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -4,7 +4,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings'
before_action :require_pages_enabled!
- before_action :authorize_update_pages!, except: [:show]
+ before_action :authorize_update_pages!
before_action :domain, except: [:new, :create]
def show
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 67827b1d3bb..6a86f8ca729 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -4,6 +4,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :authorize_read_pipeline!
+ before_action :authorize_read_build!, only: [:index]
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
@@ -69,7 +70,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: PipelineSerializer
.new(project: @project, current_user: @current_user)
- .represent(@pipeline, grouped: true)
+ .represent(@pipeline, show_represent_params)
end
end
end
@@ -157,6 +158,10 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def show_represent_params
+ { grouped: true }
+ end
+
def create_params
params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 192e6d38f36..7f988110977 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -4,6 +4,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
- redirect_to project_settings_ci_cd_path(@project, params: params)
+ redirect_to project_settings_ci_cd_path(@project, params: params.to_unsafe_h)
end
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 58d5ea4762f..4c39ee4045f 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -3,17 +3,8 @@
class Projects::ReleasesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
- before_action :check_releases_page_feature_flag
+ before_action :authorize_read_release!
def index
end
-
- private
-
- def check_releases_page_feature_flag
- return render_404 unless Feature.enabled?(:releases_page)
-
- push_frontend_feature_flag(:releases_page)
- end
end
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 0af2b7ef343..39eca10134f 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -7,19 +7,17 @@ module Projects
before_action :authorize_read_cluster!
- INDEX_PRIMING_INTERVAL = 10_000
- INDEX_POLLING_INTERVAL = 30_000
+ INDEX_PRIMING_INTERVAL = 15_000
+ INDEX_POLLING_INTERVAL = 60_000
def index
- finder = Projects::Serverless::FunctionsFinder.new(project.clusters)
-
respond_to do |format|
format.json do
functions = finder.execute
if functions.any?
Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
- render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions)
+ render json: serialize_function(functions)
else
Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
head :no_content
@@ -32,6 +30,29 @@ module Projects
end
end
end
+
+ def show
+ @service = serialize_function(finder.service(params[:environment_id], params[:id]))
+ return not_found if @service.nil?
+
+ respond_to do |format|
+ format.json do
+ render json: @service
+ end
+
+ format.html
+ end
+ end
+
+ private
+
+ def finder
+ Projects::Serverless::FunctionsFinder.new(project.clusters)
+ end
+
+ def serialize_function(function)
+ Projects::Serverless::ServiceSerializer.new(current_user: @current_user, project: project).represent(function)
+ end
end
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 75e590f3f33..f2f63e986bb 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -99,7 +99,9 @@ module Projects
def define_triggers_variables
@triggers = @project.triggers
+ .present(current_user: current_user)
@trigger = ::Ci::Trigger.new
+ .present(current_user: current_user)
end
def define_badges_variables
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
new file mode 100644
index 00000000000..7276964b6e1
--- /dev/null
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class OperationsController < Projects::ApplicationController
+ before_action :check_license
+ before_action :authorize_update_environment!
+
+ helper_method :error_tracking_setting
+
+ def show
+ end
+
+ def update
+ result = ::Projects::Operations::UpdateService.new(project, current_user, update_params).execute
+
+ render_update_response(result)
+ end
+
+ private
+
+ # overridden in EE
+ def render_update_response(result)
+ respond_to do |format|
+ format.json do
+ render_update_json_response(result)
+ end
+ end
+ end
+
+ def render_update_json_response(result)
+ if result[:status] == :success
+ flash[:notice] = _('Your changes have been saved')
+ render json: {
+ status: result[:status]
+ }
+ else
+ render(
+ status: result[:http_status] || :bad_request,
+ json: {
+ status: result[:status],
+ message: result[:message]
+ }
+ )
+ end
+ end
+
+ def error_tracking_setting
+ @error_tracking_setting ||= project.error_tracking_setting ||
+ project.build_error_tracking_setting
+ end
+
+ def update_params
+ params.require(:project).permit(permitted_project_params)
+ end
+
+ # overridden in EE
+ def permitted_project_params
+ {
+ error_tracking_setting_attributes: [
+ :enabled,
+ :api_host,
+ :token,
+ project: [:slug, :name, :organization_slug, :organization_name]
+ ]
+ }
+ end
+
+ def check_license
+ render_404 unless helpers.settings_operations_available?
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index a44acb12bdf..255f1f3569a 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -75,7 +75,14 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do
render_blob_json(blob)
end
- format.js { render 'shared/snippets/show'}
+
+ format.js do
+ if @snippet.embeddable?
+ render 'shared/snippets/show'
+ else
+ head :not_found
+ end
+ end
end
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index a50a1475eb2..a17c050b696 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -43,9 +43,22 @@ class Projects::TagsController < Projects::ApplicationController
def create
result = ::Tags::CreateService.new(@project, current_user)
- .execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
+ .execute(params[:tag_name], params[:ref], params[:message])
if result[:status] == :success
+ # Release creation with Tags was deprecated in GitLab 11.7
+ if params[:release_description].present?
+ release_params = {
+ tag: params[:tag_name],
+ name: params[:tag_name],
+ description: params[:release_description]
+ }
+
+ Releases::CreateService
+ .new(@project, current_user, release_params)
+ .execute
+ end
+
@tag = result[:tag]
redirect_to project_tag_path(@project, @tag.name)
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 3fe300dcfc0..edebfc55c17 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -31,20 +31,6 @@ class Projects::TreeController < Projects::ApplicationController
lfs_blob_ids
@last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
end
-
- format.js do
- # Disable cache so browser history works
- no_cache_headers
- end
-
- format.json do
- page_title @path.presence || _("Files"), @ref, @project.full_name
-
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
- end
- end
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index f5fdfb8accc..c7b4ebb2b24 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController
end
def trigger
- @trigger ||= project.triggers.find(params[:id]) || render_404
+ @trigger ||= project.triggers.find(params[:id])
+ .present(current_user: current_user)
end
def trigger_params
- params.require(:trigger).permit(
- :description
- )
+ params.require(:trigger).permit(:description)
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8bf93bfd68d..33c6608d321 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -6,19 +6,21 @@ class ProjectsController < Projects::ApplicationController
include ExtractsPath
include PreviewMarkdown
include SendFileUpload
+ include RecordUserLastActivity
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :whitelist_query_limiting, only: [:create]
- before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
+ before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve]
before_action :redirect_git_extension, only: [:show]
- before_action :project, except: [:index, :new, :create]
- before_action :repository, except: [:index, :new, :create]
+ before_action :project, except: [:index, :new, :create, :resolve]
+ before_action :repository, except: [:index, :new, :create, :resolve]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
+ before_action :authorize_download_code!, only: [:refs]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -441,4 +443,14 @@ class ProjectsController < Projects::ApplicationController
def present_project
@project = @project.present(current_user: current_user)
end
+
+ def resolve
+ @project = Project.find(params[:id])
+
+ if can?(current_user, :read_project, @project)
+ redirect_to @project
+ else
+ render_404
+ end
+ end
end
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index 46e382e594e..8d1847507cc 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -15,7 +15,7 @@ module Sherlock
def destroy_all
Gitlab::Sherlock.collection.clear
- redirect_to :back, status: :found
+ redirect_back_or_default(options: { status: :found })
end
end
end
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index 091bcb1253d..eee14b0faf4 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -26,10 +26,6 @@ class Snippets::NotesController < ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
alias_method :noteable, :snippet
- def note_params
- super.merge(noteable_id: params[:snippet_id])
- end
-
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dd9bf17cf0c..8ea5450b4e8 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -80,7 +80,13 @@ class SnippetsController < ApplicationController
render_blob_json(blob)
end
- format.js { render 'shared/snippets/show' }
+ format.js do
+ if @snippet.embeddable?
+ render 'shared/snippets/show'
+ else
+ head :not_found
+ end
+ end
end
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index fa5d84633b5..568c6e2a852 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -28,13 +28,13 @@ class UploadsController < ApplicationController
end
def find_model
- return nil unless params[:id]
+ return unless params[:id]
upload_model_class.find(params[:id])
end
def authorize_access!
- return nil unless model
+ return unless model
authorized =
case model
@@ -54,7 +54,7 @@ class UploadsController < ApplicationController
end
def authorize_create_access!
- return nil unless model
+ return unless model
# for now we support only personal snippets comments
authorized = can?(current_user, :comment_personal_snippet, model)
@@ -70,6 +70,10 @@ class UploadsController < ApplicationController
end
end
+ def cache_publicly?
+ User === model || Appearance === model
+ end
+
def upload_model_class
MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
end
diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb
index fbb1cfc5c66..b2799565f57 100644
--- a/app/finders/admin/runners_finder.rb
+++ b/app/finders/admin/runners_finder.rb
@@ -11,10 +11,11 @@ class Admin::RunnersFinder < UnionFinder
search!
filter_by_status!
filter_by_runner_type!
+ filter_by_tag_list!
sort!
paginate!
- @runners
+ @runners.with_tags
end
def sort_key
@@ -44,6 +45,14 @@ class Admin::RunnersFinder < UnionFinder
filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES)
end
+ def filter_by_tag_list!
+ tag_list = @params[:tag_name].presence
+
+ if tag_list
+ @runners = @runners.tagged_with(tag_list)
+ end
+ end
+
def sort!
@runners = @runners.order_by(sort_key)
end
diff --git a/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb b/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb
new file mode 100644
index 00000000000..f38c187799c
--- /dev/null
+++ b/app/finders/autocomplete/acts_as_taggable_on/tags_finder.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ module ActsAsTaggableOn
+ class TagsFinder
+ LIMIT = 20
+
+ def initialize(params:)
+ @params = params
+ end
+
+ def execute
+ tags = all_tags
+ tags = filter_by_name(tags)
+ limit(tags)
+ end
+
+ private
+
+ def all_tags
+ ::ActsAsTaggableOn::Tag.all
+ end
+
+ def filter_by_name(tags)
+ return tags unless search
+ return tags.none if search.empty?
+
+ if search.length >= Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING
+ tags.named_like(search)
+ else
+ tags.named(search)
+ end
+ end
+
+ def limit(tags)
+ tags.limit(LIMIT) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def search
+ @params[:search]
+ end
+ end
+ end
+end
diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb
index 5290313585f..8de3276184d 100644
--- a/app/finders/concerns/finder_methods.rb
+++ b/app/finders/concerns/finder_methods.rb
@@ -3,13 +3,13 @@
module FinderMethods
# rubocop: disable CodeReuse/ActiveRecord
def find_by!(*args)
- raise_not_found_unless_authorized execute.find_by!(*args)
+ raise_not_found_unless_authorized execute.reorder(nil).find_by!(*args)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def find_by(*args)
- if_authorized execute.find_by(*args)
+ if_authorized execute.reorder(nil).find_by(*args)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index c1ef9dfefa7..f8c7f0c3167 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -14,6 +14,9 @@ class ContributedProjectsFinder < UnionFinder
# Returns an ActiveRecord::Relation.
# rubocop: disable CodeReuse/ActiveRecord
def execute(current_user = nil)
+ # Do not show contributed projects if the user profile is private.
+ return Project.none unless can_read_profile?(current_user)
+
segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_id_desc
@@ -22,6 +25,10 @@ class ContributedProjectsFinder < UnionFinder
private
+ def can_read_profile?(current_user)
+ Ability.allowed?(current_user, :read_user_profile, @user)
+ end
+
def all_projects(current_user)
projects = []
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 96a36db7ec8..ec340f38450 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -134,7 +134,7 @@ class GroupDescendantsFinder
def subgroups
return Group.none unless Group.supports_nested_objects?
- # When filtering subgroups, we want to find all matches withing the tree of
+ # When filtering subgroups, we want to find all matches within the tree of
# descendants to show to the user
groups = if params[:filter]
subgroups_matching_filter
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b73a3fa6e01..072d07e0ed2 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -18,6 +18,7 @@
# assignee_id: integer or 'None' or 'Any'
# assignee_username: string
# search: string
+# in: 'title', 'description', or a string joining them with comma
# label_name: string
# sort: string
# non_archived: boolean
@@ -56,6 +57,7 @@ class IssuableFinder
milestone_title
my_reaction_emoji
search
+ in
]
end
@@ -76,13 +78,15 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # This has to be last as we may use a CTE as an optimization fence
- # by passing the attempt_group_search_optimizations param and
- # enabling the use_cte_for_group_issues_search feature flag
+ # This has to be last as we use a CTE as an optimization fence
+ # for counts by passing the force_cte param and enabling the
+ # attempt_group_search_optimizations feature flag
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
- sort(items)
+ items = sort(items) unless use_cte_for_count?
+
+ items
end
def filter_items(items)
@@ -92,6 +96,7 @@ class IssuableFinder
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
+ items = by_closed_at(items)
items = by_state(items)
items = by_group(items)
items = by_assignee(items)
@@ -114,8 +119,9 @@ class IssuableFinder
#
# rubocop: disable CodeReuse/ActiveRecord
def count_by_state
- count_params = params.merge(state: nil, sort: nil)
+ count_params = params.merge(state: nil, sort: nil, force_cte: true)
finder = self.class.new(current_user, count_params)
+
counts = Hash.new(0)
# Searching by label includes a GROUP BY in the query, but ours will be last
@@ -125,8 +131,11 @@ class IssuableFinder
#
# This does not apply when we are using a CTE for the search, as the labels
# GROUP BY is inside the subquery in that case, so we set labels_count to 1.
+ #
+ # We always use CTE when searching in Groups if the feature flag is enabled,
+ # but never when searching in Projects.
labels_count = label_names.any? ? label_names.count : 1
- labels_count = 1 if use_cte_for_search?
+ labels_count = 1 if use_cte_for_count?
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[count_key(key)] += value / labels_count
@@ -149,6 +158,18 @@ class IssuableFinder
end
end
+ def related_groups
+ if project? && project && project.group && Ability.allowed?(current_user, :read_group, project.group)
+ project.group.self_and_ancestors
+ elsif group
+ [group]
+ elsif current_user
+ Gitlab::ObjectHierarchy.new(current_user.authorized_groups, current_user.groups).all_objects
+ else
+ []
+ end
+ end
+
def project?
params[:project_id].present?
end
@@ -163,8 +184,10 @@ class IssuableFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def projects(items = nil)
- return @projects = project if project?
+ def projects
+ return @projects if defined?(@projects)
+
+ return @projects = [project] if project?
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
@@ -288,27 +311,31 @@ class IssuableFinder
def use_subquery_for_search?
strong_memoize(:use_subquery_for_search) do
- attempt_group_search_optimizations? &&
- Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false)
+ !force_cte? && attempt_group_search_optimizations?
end
end
- def use_cte_for_search?
- strong_memoize(:use_cte_for_search) do
- attempt_group_search_optimizations? &&
- !use_subquery_for_search? &&
- Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true)
+ def use_cte_for_count?
+ strong_memoize(:use_cte_for_count) do
+ force_cte? && attempt_group_search_optimizations?
end
end
private
+ def force_cte?
+ !!params[:force_cte]
+ end
+
def init_collection
klass.all
end
def attempt_group_search_optimizations?
- search && Gitlab::Database.postgresql? && params[:attempt_group_search_optimizations]
+ search &&
+ Gitlab::Database.postgresql? &&
+ params[:attempt_group_search_optimizations] &&
+ Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true)
end
def count_key(value)
@@ -337,6 +364,13 @@ class IssuableFinder
items
end
+ def by_closed_at(items)
+ items = items.closed_after(params[:closed_after]) if params[:closed_after].present?
+ items = items.closed_before(params[:closed_before]) if params[:closed_before].present?
+
+ items
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_state(items)
case params[:state].to_s
@@ -387,14 +421,14 @@ class IssuableFinder
def by_search(items)
return items unless search
- if use_cte_for_search?
+ if use_cte_for_count?
cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
cte << items
items = klass.with(cte.to_arel).from(klass.table_name)
end
- items.full_search(search)
+ items.full_search(search, matched_columns: params[:in])
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -459,7 +493,7 @@ class IssuableFinder
elsif filter_by_any_milestone?
items = items.any_milestone
elsif filter_by_upcoming_milestone?
- upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
+ upcoming_ids = Milestone.upcoming_ids(projects, related_groups)
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()')
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 45e494725d7..cb44575d6f1 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -14,6 +14,7 @@
# milestone_title: string
# assignee_id: integer
# search: string
+# in: 'title', 'description', or a string joining them with comma
# label_name: string
# sort: string
# my_reaction_emoji: string
@@ -68,7 +69,16 @@ class IssuesFinder < IssuableFinder
end
def filter_items(items)
- by_due_date(super)
+ issues = super
+ issues = by_due_date(issues)
+ issues = by_confidential(issues)
+ issues
+ end
+
+ def by_confidential(items)
+ return items if params[:confidential].nil?
+
+ params[:confidential] ? items.confidential_only : items.public_only
end
def by_due_date(items)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index e190d5d90c9..93bee3f1488 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -15,6 +15,7 @@
# author_id: integer
# assignee_id: integer
# search: string
+# in: 'title', 'description', or a string joining them with comma
# label_name: string
# sort: string
# non_archived: boolean
@@ -36,13 +37,20 @@ class MergeRequestsFinder < IssuableFinder
end
def filter_items(_items)
- items = by_source_branch(super)
+ items = by_commit(super)
+ items = by_source_branch(items)
items = by_wip(items)
by_target_branch(items)
end
private
+ def by_commit(items)
+ return items unless params[:commit_sha].presence
+
+ items.by_commit_sha(params[:commit_sha])
+ end
+
def source_branch
@source_branch ||= params[:source_branch].presence
end
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index 9c477978f60..77b55cbb838 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -3,8 +3,8 @@
# Search for milestones
#
# params - Hash
-# project_ids: Array of project ids or single project id.
-# group_ids: Array of group ids or single group id.
+# project_ids: Array of project ids or single project id or ActiveRecord relation.
+# group_ids: Array of group ids or single group id or ActiveRecord relation.
# order - Orders by field default due date asc.
# title - filter by title.
# state - filters by state.
@@ -12,20 +12,17 @@
class MilestonesFinder
include FinderMethods
- attr_reader :params, :project_ids, :group_ids
+ attr_reader :params
def initialize(params = {})
- @project_ids = Array(params[:project_ids])
- @group_ids = Array(params[:group_ids])
@params = params
end
def execute
- return Milestone.none if project_ids.empty? && group_ids.empty?
-
items = Milestone.all
items = by_groups_and_projects(items)
items = by_title(items)
+ items = by_search_title(items)
items = by_state(items)
order(items)
@@ -34,7 +31,7 @@ class MilestonesFinder
private
def by_groups_and_projects(items)
- items.for_projects_and_groups(project_ids, group_ids)
+ items.for_projects_and_groups(params[:project_ids], params[:group_ids])
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -47,6 +44,14 @@ class MilestonesFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_search_title(items)
+ if params[:search_title].present?
+ items.search_title(params[:search_title])
+ else
+ items
+ end
+ end
+
def by_state(items)
Milestone.filter_by_state(items, params[:state])
end
diff --git a/app/finders/projects/daily_statistics_finder.rb b/app/finders/projects/daily_statistics_finder.rb
new file mode 100644
index 00000000000..912c23107bc
--- /dev/null
+++ b/app/finders/projects/daily_statistics_finder.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Projects
+ class DailyStatisticsFinder
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def fetches
+ ProjectDailyStatistic.of_project(project)
+ .of_last_30_days
+ .sorted_by_date_desc
+ end
+
+ def total_fetch_count
+ fetches.sum_fetch_count
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index 2b5d67e79d7..2f2816a4a08 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -15,11 +15,40 @@ module Projects
clusters_with_knative_installed.exists?
end
+ def service(environment_scope, name)
+ knative_service(environment_scope, name)&.first
+ end
+
private
+ def knative_service(environment_scope, name)
+ clusters_with_knative_installed.preload_knative.map do |cluster|
+ next if environment_scope != cluster.environment_scope
+
+ services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
+ .select { |svc| svc["metadata"]["name"] == name }
+
+ add_metadata(cluster, services).first unless services.nil?
+ end
+ end
+
def knative_services
clusters_with_knative_installed.preload_knative.map do |cluster|
- cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
+ services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace)
+ add_metadata(cluster, services) unless services.nil?
+ end
+ end
+
+ def add_metadata(cluster, services)
+ services.each do |s|
+ s["environment_scope"] = cluster.environment_scope
+ s["cluster_id"] = cluster.id
+
+ if services.length == 1
+ s["podcount"] = cluster.application_knative.service_pod_details(
+ cluster.platform_kubernetes&.actual_namespace,
+ s["metadata"]["name"]).length
+ end
end
end
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
new file mode 100644
index 00000000000..59e84198fde
--- /dev/null
+++ b/app/finders/releases_finder.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ReleasesFinder
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ def execute
+ return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
+
+ @project.releases.sorted
+ end
+end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index d3774746cb8..bf29f15642d 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -69,6 +69,8 @@ class SnippetsFinder < UnionFinder
base.with_optional_visibility(visibility_from_scope).fresh
end
+ private
+
# Produces a query that retrieves snippets from multiple projects.
#
# The resulting query will, depending on the user's permissions, include the
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
index 54f01c99d78..7d0cb777ad1 100644
--- a/app/graphql/mutations/merge_requests/base.rb
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -25,7 +25,8 @@ module Mutations
def find_object(project_path:, iid:)
project = resolve_project(full_path: project_path)
- resolver = Resolvers::MergeRequestResolver.new(object: project, context: context)
+ resolver = Resolvers::MergeRequestsResolver
+ .single.new(object: project, context: context)
resolver.resolve(iid: iid)
end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 459933af9d3..063def75d38 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -2,5 +2,12 @@
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
+ def self.single
+ @single ||= Class.new(self) do
+ def resolve(**args)
+ super.first
+ end
+ end
+ end
end
end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 4ab3c13787a..b98d8bd1fff 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -2,8 +2,37 @@
module Resolvers
class IssuesResolver < BaseResolver
- extend ActiveSupport::Concern
+ argument :iid, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The IID of the issue, e.g., "1"'
+ argument :iids, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The list of IIDs of issues, e.g., [1, 2]'
+ argument :state, Types::IssuableStateEnum,
+ required: false,
+ description: "Current state of Issue"
+ argument :label_name, GraphQL::STRING_TYPE.to_list_type,
+ required: false,
+ description: "Labels applied to the Issue"
+ argument :created_before, Types::TimeType,
+ required: false,
+ description: "Issues created before this date"
+ argument :created_after, Types::TimeType,
+ required: false,
+ description: "Issues created after this date"
+ argument :updated_before, Types::TimeType,
+ required: false,
+ description: "Issues updated before this date"
+ argument :updated_after, Types::TimeType,
+ required: false,
+ description: "Issues updated after this date"
+ argument :closed_before, Types::TimeType,
+ required: false,
+ description: "Issues closed before this date"
+ argument :closed_after, Types::TimeType,
+ required: false,
+ description: "Issues closed after this date"
argument :search, GraphQL::STRING_TYPE,
required: false
argument :sort, Types::Sort,
@@ -18,6 +47,7 @@ module Resolvers
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54520
args[:project_id] = project.id
+ args[:iids] ||= [args[:iid]].compact
IssuesFinder.new(context[:current_user], args).execute
end
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
deleted file mode 100644
index d047ce9e3a1..00000000000
--- a/app/graphql/resolvers/merge_request_resolver.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- class MergeRequestResolver < BaseResolver
- argument :iid, GraphQL::ID_TYPE,
- required: true,
- description: 'The IID of the merge request, e.g., "1"'
-
- type Types::MergeRequestType, null: true
-
- alias_method :project, :object
-
- # rubocop: disable CodeReuse/ActiveRecord
- def resolve(iid:)
- return unless project.present?
-
- BatchLoader.for(iid.to_s).batch(key: project) do |iids, loader, args|
- args[:key].merge_requests.where(iid: iids).each do |mr|
- loader.call(mr.iid.to_s, mr)
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
new file mode 100644
index 00000000000..90795c797ac
--- /dev/null
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class MergeRequestsResolver < BaseResolver
+ argument :iid, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The IID of the merge request, e.g., "1"'
+
+ argument :iids, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The list of IIDs of issues, e.g., [1, 2]'
+
+ type Types::MergeRequestType, null: true
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ return unless project.present?
+
+ args[:iids] ||= [args[:iid]].compact
+
+ args[:iids].map { |iid| batch_load(iid) }
+ .select(&:itself) # .compact doesn't work on BatchLoader
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def batch_load(iid)
+ BatchLoader.for(iid.to_s).batch(key: project) do |iids, loader, args|
+ args[:key].merge_requests.where(iid: iids).each do |mr|
+ loader.call(mr.iid.to_s, mr)
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/graphql/resolvers/metadata_resolver.rb b/app/graphql/resolvers/metadata_resolver.rb
new file mode 100644
index 00000000000..3a79e6434fb
--- /dev/null
+++ b/app/graphql/resolvers/metadata_resolver.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class MetadataResolver < BaseResolver
+ type Types::MetadataType, null: false
+
+ def resolve(**args)
+ { version: Gitlab::VERSION, revision: Gitlab.revision }
+ end
+ end
+end
diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb
new file mode 100644
index 00000000000..f2f6d6c6cab
--- /dev/null
+++ b/app/graphql/types/issuable_state_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ class IssuableStateEnum < BaseEnum
+ graphql_name 'IssuableState'
+ description 'State of a GitLab issue or merge request'
+
+ value 'opened'
+ value 'closed'
+ value 'locked'
+ end
+end
diff --git a/app/graphql/types/issue_state_enum.rb b/app/graphql/types/issue_state_enum.rb
new file mode 100644
index 00000000000..6521407fc9d
--- /dev/null
+++ b/app/graphql/types/issue_state_enum.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Types
+ class IssueStateEnum < IssuableStateEnum
+ graphql_name 'IssueState'
+ description 'State of a GitLab issue'
+ end
+end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index a8f2f7914a8..5ad3ea52930 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -11,22 +11,20 @@ module Types
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
- field :state, GraphQL::STRING_TYPE, null: false
+ field :state, IssueStateEnum, null: false
field :author, Types::UserType,
null: false,
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find } do
- authorize :read_user
- end
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find },
+ authorize: :read_user
field :assignees, Types::UserType.connection_type, null: true
field :labels, Types::LabelType.connection_type, null: true
field :milestone, Types::MilestoneType,
null: true,
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find } do
- authorize :read_milestone
- end
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find },
+ authorize: :read_milestone
field :due_date, Types::TimeType, null: true
field :confidential, GraphQL::BOOLEAN_TYPE, null: false
diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb
new file mode 100644
index 00000000000..92f52726ab3
--- /dev/null
+++ b/app/graphql/types/merge_request_state_enum.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ class MergeRequestStateEnum < IssuableStateEnum
+ graphql_name 'MergeRequestState'
+ description 'State of a GitLab merge request'
+
+ value 'merged'
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index fb740b6fb1c..1ed27a14e33 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -12,7 +12,7 @@ module Types
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
field :description, GraphQL::STRING_TYPE, null: true
- field :state, GraphQL::STRING_TYPE, null: true
+ field :state, MergeRequestStateEnum, null: false
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
field :source_project, Types::ProjectType, null: true
@@ -38,8 +38,8 @@ module Types
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true
field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false
- field :diff_head_sha, GraphQL::STRING_TYPE, null: true
- field :merge_commit_message, GraphQL::STRING_TYPE, null: true
+ field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage"
+ field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false
field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false
field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true
@@ -48,9 +48,7 @@ module Types
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
- field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline do
- authorize :read_pipeline
- end
+ field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline, authorize: :read_pipeline
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
end
diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb
new file mode 100644
index 00000000000..2d8bad0614b
--- /dev/null
+++ b/app/graphql/types/metadata_type.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ class MetadataType < ::Types::BaseObject
+ graphql_name 'Metadata'
+
+ field :version, GraphQL::STRING_TYPE, null: false
+ field :revision, GraphQL::STRING_TYPE, null: false
+ end
+end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index ab37c282fe5..e9a4ea9157b 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -8,7 +8,7 @@ module Types
abilities :change_namespace, :change_visibility_level, :rename_project,
:remove_project, :archive_project, :remove_fork_project,
:remove_pages, :read_project, :create_merge_request_in,
- :read_wiki, :read_project_member, :create_issue, :upload_file,
+ :read_wiki, :read_project_member, :create_issue, :upload_file,
:read_cycle_analytics, :download_code, :download_wiki_code,
:fork_project, :create_project_snippet, :read_commit_status,
:request_access, :create_pipeline, :create_pipeline_schedule,
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 050706f97be..b96c2f3afb2 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -16,7 +16,6 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true
- field :default_branch, GraphQL::STRING_TYPE, null: true
field :tag_list, GraphQL::STRING_TYPE, null: true
field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
@@ -59,25 +58,34 @@ module Types
end
field :import_status, GraphQL::STRING_TYPE, null: true
- field :ci_config_path, GraphQL::STRING_TYPE, null: true
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :merge_requests,
+ Types::MergeRequestType.connection_type,
+ null: true,
+ resolver: Resolvers::MergeRequestsResolver,
+ authorize: :read_merge_request
+
field :merge_request,
Types::MergeRequestType,
null: true,
- resolver: Resolvers::MergeRequestResolver do
- authorize :read_merge_request
- end
+ resolver: Resolvers::MergeRequestsResolver.single,
+ authorize: :read_merge_request
field :issues,
Types::IssueType.connection_type,
null: true,
resolver: Resolvers::IssuesResolver
+ field :issue,
+ Types::IssueType,
+ null: true,
+ resolver: Resolvers::IssuesResolver.single
+
field :pipelines,
Types::Ci::PipelineType.connection_type,
null: false,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 7c41716b82a..472fe5d6ec2 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -1,14 +1,21 @@
# frozen_string_literal: true
module Types
- class QueryType < BaseObject
+ class QueryType < ::Types::BaseObject
graphql_name 'Query'
field :project, Types::ProjectType,
null: true,
resolver: Resolvers::ProjectResolver,
- description: "Find a project" do
- authorize :read_project
+ description: "Find a project",
+ authorize: :read_project
+
+ field :metadata, Types::MetadataType,
+ null: true,
+ resolver: Resolvers::MetadataResolver,
+ description: 'Metadata about GitLab' do |*args|
+
+ authorize :read_instance_metadata
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 3f69af50f25..c0db9910143 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module AppearancesHelper
+ include MarkupHelper
+
def brand_title
current_appearance&.title.presence || default_brand_title
end
@@ -11,7 +13,7 @@ module AppearancesHelper
end
def brand_image
- image_tag(current_appearance.logo) if current_appearance&.logo?
+ image_tag(current_appearance.logo_path) if current_appearance&.logo?
end
def brand_text
@@ -28,7 +30,7 @@ module AppearancesHelper
def brand_header_logo
if current_appearance&.header_logo?
- image_tag current_appearance.header_logo
+ image_tag current_appearance.header_logo_path, class: 'brand-header-logo'
else
render 'shared/logo.svg'
end
@@ -40,4 +42,36 @@ module AppearancesHelper
render 'shared/logo_type.svg'
end
end
+
+ def header_message
+ return unless current_appearance&.show_header?
+
+ class_names = []
+ class_names << 'with-performance-bar' if performance_bar_enabled?
+
+ render_message(:header_message, class_names: class_names)
+ end
+
+ def footer_message
+ return unless current_appearance&.show_footer?
+
+ render_message(:footer_message)
+ end
+
+ private
+
+ def render_message(field_sym, class_names: [], style: message_style)
+ class_names << field_sym.to_s.dasherize
+
+ content_tag :div, class: class_names, style: style do
+ markdown_field(current_appearance, field_sym)
+ end
+ end
+
+ def message_style
+ style = []
+ style << "background-color: #{current_appearance.message_background_color};"
+ style << "color: #{current_appearance.message_font_color}"
+ style.join
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 82bb2d1a805..ffa5719fefb 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -214,12 +214,19 @@ module ApplicationHelper
class_names = []
class_names << 'issue-boards-page' if current_controller?(:boards)
class_names << 'with-performance-bar' if performance_bar_enabled?
-
+ class_names << system_message_class
class_names
end
- # EE feature: System header and footer, unavailable in CE
def system_message_class
+ class_names = []
+
+ return class_names unless appearance
+
+ class_names << 'with-system-header' if appearance.show_header?
+ class_names << 'with-system-footer' if appearance.show_footer?
+
+ class_names
end
# Returns active css class when condition returns true
@@ -268,6 +275,17 @@ module ApplicationHelper
_('You are on a read-only GitLab instance.')
end
+ def client_class_list
+ "gl-browser-#{browser.id} gl-platform-#{browser.platform.id}"
+ end
+
+ def client_js_flags
+ {
+ "is#{browser.id.to_s.titlecase}": true,
+ "is#{browser.platform.id.to_s.titlecase}": true
+ }
+ end
+
def autocomplete_data_sources(object, noteable_type)
return {} unless object && noteable_type
@@ -281,4 +299,10 @@ module ApplicationHelper
snippets: snippets_project_autocomplete_sources_path(object)
}
end
+
+ private
+
+ def appearance
+ ::Appearance.current
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 5a7c005fd06..e635f608237 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -20,7 +20,7 @@ module ApplicationSettingsHelper
def enabled_protocol
case Gitlab::CurrentSettings.enabled_git_access_protocol
when 'http'
- gitlab_config.protocol
+ Gitlab.config.gitlab.protocol
when 'ssh'
'ssh'
end
@@ -35,7 +35,7 @@ module ApplicationSettingsHelper
end
def http_enabled?
- all_protocols_enabled? || enabled_protocol == 'http'
+ all_protocols_enabled? || Gitlab::CurrentSettings.enabled_git_access_protocol == 'http'
end
def enabled_project_button(project, protocol)
@@ -150,6 +150,7 @@ module ApplicationSettingsHelper
:email_author_in_body,
:enabled_git_access_protocol,
:enforce_terms,
+ :first_day_of_week,
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
@@ -231,7 +232,8 @@ module ApplicationSettingsHelper
:web_ide_clientside_preview_enabled,
:diff_max_patch_bytes,
:commit_email_hostname,
- :protected_ci_variables
+ :protected_ci_variables,
+ :local_markdown_version
]
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 654fb9d9987..2b1d6f49878 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -16,6 +16,13 @@ module AuthHelper
PROVIDERS_WITH_ICONS.include?(name.to_s)
end
+ def qa_class_for_provider(provider)
+ {
+ saml: 'qa-saml-login-button',
+ github: 'qa-github-login-button'
+ }[provider.to_sym]
+ end
+
def auth_providers
Gitlab::Auth::OAuth::Provider.providers
end
@@ -31,7 +38,7 @@ module AuthHelper
def form_based_provider_with_highest_priority
@form_based_provider_with_highest_priority ||= begin
form_based_provider_priority.each do |provider_regexp|
- highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
+ highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
break highest_priority unless highest_priority.nil?
end
end
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 516c8a353ea..67e7e475920 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -9,41 +9,4 @@ module AutoDevopsHelper
!project.repository.gitlab_ci_yml &&
!project.ci_service
end
-
- def auto_devops_warning_message(project)
- if missing_auto_devops_service?(project)
- params = {
- kubernetes: link_to('Kubernetes cluster', project_clusters_path(project))
- }
-
- if missing_auto_devops_domain?(project)
- _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params
- else
- _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params
- end
- elsif missing_auto_devops_domain?(project)
- _('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def cluster_ingress_ip(project)
- project
- .cluster_ingresses
- .where("external_ip is not null")
- .limit(1)
- .pluck(:external_ip)
- .first
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def missing_auto_devops_domain?(project)
- !(project.auto_devops || project.build_auto_devops)&.has_domain?
- end
-
- def missing_auto_devops_service?(project)
- !project.deployment_platform&.active?
- end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index bd42f00944f..3e1bb9af5cc 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -31,12 +31,13 @@ module BlobHelper
edit_button_tag(blob,
common_classes,
_('Edit'),
- edit_blob_path(project, ref, path, options),
+ Feature.enabled?(:web_ide_default) ? ide_edit_path(project, ref, path, options) : edit_blob_path(project, ref, path, options),
project,
ref)
end
def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
+ return if Feature.enabled?(:web_ide_default)
return unless blob = readable_blob(options, path, project, ref)
edit_button_tag(blob,
@@ -140,36 +141,6 @@ module BlobHelper
Gitlab::Sanitizers::SVG.clean(data)
end
- # Remove once https://gitlab.com/gitlab-org/gitlab-ce/issues/36103 is closed
- # and :workhorse_set_content_type flag is removed
- # If we blindly set the 'real' content type when serving a Git blob we
- # are enabling XSS attacks. An attacker could upload e.g. a Javascript
- # file to a Git repository, trick the browser of a victim into
- # downloading the blob, and then the 'application/javascript' content
- # type would tell the browser to execute the attacker's Javascript. By
- # overriding the content type and setting it to 'text/plain' (in the
- # example of Javascript) we tell the browser of the victim not to
- # execute untrusted data.
- def safe_content_type(blob)
- if blob.extension == 'svg'
- blob.mime_type
- elsif blob.text?
- 'text/plain; charset=utf-8'
- elsif blob.image?
- blob.content_type
- else
- 'application/octet-stream'
- end
- end
-
- def content_disposition(blob, inline)
- # Remove the following line when https://gitlab.com/gitlab-org/gitlab-ce/issues/36103
- # is closed and :workhorse_set_content_type flag is removed
- return 'attachment' if blob.extension == 'svg'
-
- inline ? 'inline' : 'attachment'
- end
-
def ref_project
@ref_project ||= @target_project || @project
end
@@ -207,7 +178,8 @@ module BlobHelper
'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-filename' => @blob && @blob.path,
- 'project-id' => project.id
+ 'project-id' => project.id,
+ 'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path)
}
end
@@ -223,7 +195,7 @@ module BlobHelper
def open_raw_blob_button(blob)
return if blob.empty?
- return if blob.raw_binary? || blob.stored_externally?
+ return if blob.binary? || blob.stored_externally?
title = 'Open raw'
link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 923a06a0512..355b91a8661 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -100,17 +100,6 @@ module CiStatusHelper
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
end
- def render_project_pipeline_status(pipeline_status, tooltip_placement: 'left')
- project = pipeline_status.project
- path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
-
- render_status_with_link(
- 'commit',
- pipeline_status.status,
- path,
- tooltip_placement: tooltip_placement)
- end
-
def render_commit_status(commit, ref: nil, tooltip_placement: 'left')
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
@@ -123,12 +112,6 @@ module CiStatusHelper
icon_size: 24)
end
- def render_pipeline_status(pipeline, tooltip_placement: 'left')
- project = pipeline.project
- path = project_pipeline_path(project, pipeline)
- render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
- end
-
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d52cfd6e37a..d58f634425b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -154,7 +154,7 @@ module CommitsHelper
if user.nil?
mail_to(source_email, text, link_options)
else
- link_to(text, user_path(user), link_options)
+ link_to(text, user_path(user), { class: "commit-#{options[:source]}-link js-user-link", data: { user_id: user.id } })
end
end
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
index 13839474e1f..62bb2e4da23 100644
--- a/app/helpers/count_helper.rb
+++ b/app/helpers/count_helper.rb
@@ -13,7 +13,7 @@ module CountHelper
# memberships, and deducting 1 for each root of the fork network.
# This might be inacurate as the root of the fork network might have been deleted.
#
- # This makes querying this information a lot more effecient and it should be
+ # This makes querying this information a lot more efficient and it should be
# accurate enough for the instance wide statistics
def approximate_fork_count_with_delimiters(count_data)
fork_network_count = count_data[ForkNetwork]
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b6844d36052..32431959851 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -138,30 +138,6 @@ module DiffHelper
!diff_file.deleted_file? && @merge_request && @merge_request.source_project
end
- def diff_render_error_reason(viewer)
- case viewer.render_error
- when :too_large
- "it is too large"
- when :server_side_but_stored_externally
- case viewer.diff_file.external_storage
- when :lfs
- 'it is stored in LFS'
- else
- 'it is stored externally'
- end
- end
- end
-
- def diff_render_error_options(viewer)
- diff_file = viewer.diff_file
- options = []
-
- blob_url = project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.file_path))
- options << link_to('view the blob', blob_url)
-
- options
- end
-
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file?
"file-deletion"
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index e4c46ceeaa2..96471d15aac 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -36,6 +36,14 @@ module EmailsHelper
nil
end
+ def sanitize_name(name)
+ if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF]
+ name.tr('.', '_')
+ else
+ name
+ end
+ end
+
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
@@ -58,7 +66,7 @@ module EmailsHelper
def header_logo
if current_appearance&.header_logo?
image_tag(
- current_appearance.header_logo,
+ current_appearance.header_logo_path,
style: 'height: 50px'
)
else
@@ -123,4 +131,42 @@ module EmailsHelper
project.id.to_s + "." + project_path_as_domain + "." + Gitlab.config.gitlab.host
end
+
+ def html_header_message
+ return unless show_header?
+
+ render_message(:header_message, style: '')
+ end
+
+ def html_footer_message
+ return unless show_footer?
+
+ render_message(:footer_message, style: '')
+ end
+
+ def text_header_message
+ return unless show_header?
+
+ strip_tags(render_message(:header_message, style: ''))
+ end
+
+ def text_footer_message
+ return unless show_footer?
+
+ strip_tags(render_message(:footer_message, style: ''))
+ end
+
+ private
+
+ def show_footer?
+ email_header_and_footer_enabled? && current_appearance&.show_footer?
+ end
+
+ def show_header?
+ email_header_and_footer_enabled? && current_appearance&.show_header?
+ end
+
+ def email_header_and_footer_enabled?
+ current_appearance&.email_header_and_footer_enabled?
+ end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 7b22bc8f98f..365b94f5a3e 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -7,6 +7,14 @@ module EnvironmentsHelper
}
end
+ def environments_folder_list_view_data
+ {
+ "endpoint" => folder_project_environments_path(@project, @folder, format: :json),
+ "folder-name" => @folder,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s
+ }
+ end
+
def metrics_data(project, environment)
{
"settings-path" => edit_project_service_path(project, 'prometheus'),
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
deleted file mode 100644
index e36d63b2946..00000000000
--- a/app/helpers/external_wiki_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module ExternalWikiHelper
- def get_project_wiki_path(project)
- external_wiki_service = project.external_wiki
- if external_wiki_service
- external_wiki_service.properties['external_wiki_url']
- else
- project_wiki_path(project, :home)
- end
- end
-end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 49171df1433..3d494c3de6a 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -8,7 +8,9 @@ module ImportHelper
end
def sanitize_project_name(name)
- name.gsub(/[^\w\-]/, '-')
+ # For personal projects in Bitbucket in the form ~username, we can
+ # just drop that leading tilde.
+ name.gsub(/\A~+/, '').gsub(/[^\w\-]/, '-')
end
def import_project_target(owner, name)
@@ -16,10 +18,8 @@ module ImportHelper
"#{namespace}/#{name}"
end
- def provider_project_link(provider, full_path)
- url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend
-
- link_to full_path, url, target: '_blank', rel: 'noopener noreferrer'
+ def provider_project_link_url(provider_url, full_path)
+ Gitlab::Utils.append_path(provider_url, full_path)
end
def import_will_timeout_message(_ci_cd_only)
@@ -44,10 +44,6 @@ module ImportHelper
_('Please wait while we import the repository for you. Refresh at will.')
end
- def import_github_title
- _('Import repositories from GitHub')
- end
-
def import_github_authorize_message
_('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:')
end
@@ -71,30 +67,4 @@ module ImportHelper
_('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
end
end
-
- def import_githubish_choose_repository_message
- _('Choose which repositories you want to import.')
- end
-
- def import_all_githubish_repositories_button_label
- _('Import all repositories')
- end
-
- private
-
- def github_project_url(full_path)
- Gitlab::Utils.append_path(github_root_url, full_path)
- end
-
- def github_root_url
- strong_memoize(:github_url) do
- provider = Gitlab::Auth::OAuth::Provider.config_for('github')
-
- provider&.dig('url').presence || 'https://github.com'
- end
- end
-
- def gitea_project_url(full_path)
- Gitlab::Utils.append_path(@gitea_host_url, full_path)
- end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 5f7147508c7..0622cdfc196 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -191,7 +191,7 @@ module IssuablesHelper
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline")
- author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
+ author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none")
if status = user_status(issuable.author)
author_output << "#{status}".html_safe
@@ -200,7 +200,7 @@ module IssuablesHelper
author_output
end
- output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
+ output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip prepend-left-4', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
@@ -209,7 +209,7 @@ module IssuablesHelper
end
def issuable_labels_tooltip(labels, limit: 5)
- first, last = labels.partition.with_index { |_, i| i < limit }
+ first, last = labels.partition.with_index { |_, i| i < limit }
if labels && labels.any?
label_names = first.collect { |label| label.fetch(:title) }
@@ -268,7 +268,7 @@ module IssuablesHelper
issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
- markdownVersion: issuable.cached_markdown_version,
+ lockVersion: issuable.lock_version,
issuableTemplates: issuable_templates(issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 39f661b5f0c..bd53add80ca 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -227,6 +227,10 @@ module LabelsHelper
"#{action} at #{level} level"
end
+ def labels_sorted_by_title(labels)
+ labels.sort_by(&:title)
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 0d638b850b4..d83c69603a9 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -74,7 +74,7 @@ module MarkupHelper
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
md = markdown_field(object, attribute, options)
- return nil unless md.present?
+ return unless md.present?
tags = %w(a gl-emoji b pre code p span)
tags << 'img' if options[:allow_images]
@@ -116,7 +116,6 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
- context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
@@ -132,7 +131,6 @@ module MarkupHelper
page_slug: wiki_page.slug,
issuable_state_filter_enabled: true
)
- context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html =
case wiki_page.format
@@ -187,10 +185,6 @@ module MarkupHelper
end
end
- def commonmark_for_repositories_enabled?
- Feature.enabled?(:commonmark_for_repositories, default_enabled: true)
- end
-
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 5a21403bc5e..11d5591d509 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -18,12 +18,13 @@ module MembersHelper
"remove #{member.user.name} from"
end
- "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user'
- "#{action} from #{member.real_source_type.humanize(capitalize: false)}"
+
+ "#{action} from #{source_text(member)}"
end
def leave_confirmation_message(member_source)
@@ -32,7 +33,17 @@ module MembersHelper
end
def filter_group_project_member_path(options = {})
- options = params.slice(:search, :sort).merge(options)
+ options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}"
end
+
+ private
+
+ def source_text(member)
+ type = member.real_source_type.humanize(capitalize: false)
+
+ return type if member.request? || member.invite? || type != 'group'
+
+ 'group and any subresources'
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 23d7aa427bb..991ca42c445 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -29,7 +29,7 @@ module MergeRequestsHelper
def ci_build_details_path(merge_request)
build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch)
- return nil unless build_url
+ return unless build_url
parsed_url = URI.parse(build_url)
@@ -92,7 +92,7 @@ module MergeRequestsHelper
end
def version_index(merge_request_diff)
- return nil if @merge_request_diffs.empty?
+ return if @merge_request_diffs.empty?
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
@@ -149,7 +149,7 @@ module MergeRequestsHelper
def merge_request_source_project_for_project(project = @project)
unless can?(current_user, :create_merge_request_in, project)
- return nil
+ return
end
if can?(current_user, :create_merge_request_from, project)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 327b69e5110..50aec83b867 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -231,12 +231,15 @@ module MilestonesHelper
end
end
- def group_or_dashboard_milestone_path(milestone)
- if milestone.group_milestone?
- group_milestone_path(milestone.group, milestone.iid, milestone: { title: milestone.title })
- else
- dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- end
+ def group_or_project_milestone_path(milestone)
+ params =
+ if milestone.group_milestone?
+ { milestone: { title: milestone.title } }
+ else
+ { title: milestone.title }
+ end
+
+ milestone_path(milestone.milestone, params)
end
def can_admin_project_milestones?
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 6c65e573307..ea3bcfc791a 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -5,11 +5,8 @@ module NamespacesHelper
params.dig(:project, :namespace_id) || params[:namespace_id]
end
- # rubocop: disable CodeReuse/ActiveRecord
def namespaces_options(selected = :current_user, display_path: false, groups: nil, extra_group: nil, groups_only: false)
- groups ||= current_user.manageable_groups
- .eager_load(:route)
- .order('routes.path')
+ groups ||= current_user.manageable_groups_with_routes
users = [current_user.namespace]
selected_id = selected
@@ -43,7 +40,6 @@ module NamespacesHelper
grouped_options_for_select(options, selected_id)
end
- # rubocop: enable CodeReuse/ActiveRecord
def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 033686823a2..a50137bea3d 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -85,7 +85,7 @@ module NotesHelper
diffs_project_merge_request_path(discussion.project, discussion.noteable, path_params)
elsif discussion.for_commit?
- anchor = discussion.line_code if discussion.diff_discussion?
+ anchor = discussion.diff_discussion? ? discussion.line_code : "note_#{discussion.first_note.id}"
project_commit_path(discussion.project, discussion.noteable, anchor: anchor)
end
@@ -122,7 +122,7 @@ module NotesHelper
end
def new_form_url
- return nil unless @snippet.is_a?(PersonalSnippet)
+ return unless @snippet.is_a?(PersonalSnippet)
snippet_notes_path(@snippet)
end
@@ -171,7 +171,6 @@ module NotesHelper
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
- markdownVersion: issuable.cached_markdown_version,
quickActionsDocsPath: help_page_path('user/project/quick_actions'),
closePath: close_issuable_path(issuable),
reopenPath: reopen_issuable_path(issuable),
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index f4f46b0fe96..766508b6609 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -43,6 +43,18 @@ module PreferencesHelper
]
end
+ def first_day_of_week_choices
+ [
+ [_('Sunday'), 0],
+ [_('Monday'), 1],
+ [_('Saturday'), 6]
+ ]
+ end
+
+ def first_day_of_week_choices_with_default
+ first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil])
+ end
+
def user_application_theme
@user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
end
@@ -51,6 +63,10 @@ module PreferencesHelper
Gitlab::ColorSchemes.for_user(current_user).css_class
end
+ def language_choices
+ Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }
+ end
+
private
# Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too
@@ -66,4 +82,8 @@ module PreferencesHelper
def excluded_dashboard_choices
['operations']
end
+
+ def default_first_day_of_week
+ first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first
+ end
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index df318de740a..5a42e581867 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -25,4 +25,8 @@ module ProfilesHelper
end
end
end
+
+ def user_profile?
+ params[:controller] == 'users'
+ end
end
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
new file mode 100644
index 00000000000..6daf2e21ca2
--- /dev/null
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects::ErrorTrackingHelper
+ def error_tracking_data(project)
+ error_tracking_enabled = !!project.error_tracking_setting&.enabled?
+
+ {
+ 'index-path' => project_error_tracking_index_path(project,
+ format: :json),
+ 'enable-error-tracking-link' => project_settings_operations_path(project),
+ 'error-tracking-enabled' => error_tracking_enabled.to_s,
+ 'illustration-path' => image_path('illustrations/cluster_popover.svg')
+ }
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0cfc2db3285..2ac8ddc5244 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -204,12 +204,10 @@ module ProjectsHelper
current_user.require_extra_setup_for_git_auth?
end
- def show_auto_devops_implicitly_enabled_banner?(project)
- cookie_key = "hide_auto_devops_implicitly_enabled_banner_#{project.id}"
+ def show_auto_devops_implicitly_enabled_banner?(project, user)
+ return false unless user_can_see_auto_devops_implicitly_enabled_banner?(project, user)
- project.has_auto_devops_implicitly_enabled? &&
- cookies[cookie_key.to_sym].blank? &&
- (project.owner == current_user || project.team.maintainer?(current_user))
+ cookies["hide_auto_devops_implicitly_enabled_banner_#{project.id}".to_sym].blank?
end
def link_to_set_password
@@ -267,10 +265,6 @@ module ProjectsHelper
link_to 'BFG', 'https://rtyley.github.io/bfg-repo-cleaner/', target: '_blank', rel: 'noopener noreferrer'
end
- def legacy_render_context(params)
- params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
- end
-
def explore_projects_tab?
current_page?(explore_projects_path) ||
current_page?(trending_explore_projects_path) ||
@@ -285,6 +279,25 @@ module ProjectsHelper
!disabled && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
end
+ # overridden in EE
+ def settings_operations_available?
+ can?(current_user, :read_environment, @project)
+ end
+
+ def error_tracking_setting_project_json
+ setting = @project.error_tracking_setting
+
+ return if setting.blank? || setting.project_slug.blank? ||
+ setting.organization_slug.blank?
+
+ {
+ name: setting.project_name,
+ organization_name: setting.organization_name,
+ organization_slug: setting.organization_slug,
+ slug: setting.project_slug
+ }.to_json
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -302,7 +315,8 @@ module ProjectsHelper
nav_tabs << :container_registry
end
- if project.builds_enabled? && can?(current_user, :read_pipeline, project)
+ # Pipelines feature is tied to presence of builds
+ if can?(current_user, :read_build, project)
nav_tabs << :pipelines
end
@@ -310,19 +324,24 @@ module ProjectsHelper
nav_tabs << :operations
end
- if project.external_issue_tracker
- nav_tabs << :external_issue_tracker
- end
-
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
+ nav_tabs << external_nav_tabs(project)
+
nav_tabs.flatten
end
+ def external_nav_tabs(project)
+ [].tap do |tabs|
+ tabs << :external_issue_tracker if project.external_issue_tracker
+ tabs << :external_wiki if project.external_wiki
+ end
+ end
+
def tab_ability_map
{
environments: :read_environment,
@@ -332,6 +351,7 @@ module ProjectsHelper
builds: :read_build,
clusters: :read_cluster,
serverless: :read_cluster,
+ error_tracking: :read_sentry_issue,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -485,7 +505,7 @@ module ProjectsHelper
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'),
pagesAvailable: Gitlab.config.pages.enabled,
pagesAccessControlEnabled: Gitlab.config.pages.access_control,
- pagesHelpPath: help_page_path('user/project/pages/index.md')
+ pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control-core-only')
}
end
@@ -546,6 +566,7 @@ module ProjectsHelper
services#edit
repository#show
ci_cd#show
+ operations#show
badges#index
pages#show
]
@@ -575,8 +596,16 @@ module ProjectsHelper
environments
clusters
functions
+ error_tracking
user
gcp
]
end
+
+ def user_can_see_auto_devops_implicitly_enabled_banner?(project, user)
+ Ability.allowed?(user, :admin_project, project) &&
+ project.has_auto_devops_implicitly_enabled? &&
+ project.builds_enabled? &&
+ !project.repository.gitlab_ci_yml
+ end
end
diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb
index cb21f922401..0d880c38a7b 100644
--- a/app/helpers/runners_helper.rb
+++ b/app/helpers/runners_helper.rb
@@ -28,4 +28,14 @@ module RunnersHelper
display_name + id
end
end
+
+ # Due to inability of performing sorting of runners by cached "contacted_at" values we have to show uncached values if sorting by "contacted_asc" is requested.
+ # Please refer to the following issue for more details: https://gitlab.com/gitlab-org/gitlab-ce/issues/55920
+ def runner_contacted_at(runner)
+ if params[:sort] == 'contacted_asc'
+ runner.uncached_contacted_at
+ else
+ runner.contacted_at
+ end
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 80cc568820a..0ee76a51f7d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -24,10 +24,10 @@ module SearchHelper
end
def search_entries_info(collection, scope, term)
- return unless collection.count > 0
+ return if collection.to_a.empty?
from = collection.offset_value + 1
- to = collection.offset_value + collection.count
+ to = collection.offset_value + collection.to_a.size
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index c7d31f3469d..ecb2b2d707b 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -110,7 +110,7 @@ module SnippetsHelper
def embedded_snippet_raw_button
blob = @snippet.blob
- return if blob.empty? || blob.raw_binary? || blob.stored_externally?
+ return if blob.empty? || blob.binary? || blob.stored_externally?
snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet)
@@ -130,12 +130,4 @@ module SnippetsHelper
link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
end
-
- def public_snippet?
- if @snippet.project_id?
- can?(nil, :read_project_snippet, @snippet)
- else
- can?(nil, :read_personal_snippet, @snippet)
- end
- end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 6ac1f42c321..07ec129dea3 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -128,7 +128,9 @@ module SortingHelper
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_updated => sort_title_recently_updated,
- sort_value_oldest_updated => sort_title_oldest_updated
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_last_activity => sort_title_recently_last_activity,
+ sort_value_oldest_last_activity => sort_title_oldest_last_activity
}
end
@@ -234,7 +236,7 @@ module SortingHelper
end
def sort_title_milestone
- s_('SortOptions|Milestone')
+ s_('SortOptions|Milestone due date')
end
def sort_title_milestone_later
@@ -317,6 +319,14 @@ module SortingHelper
s_('SortOptions|Most stars')
end
+ def sort_title_oldest_last_activity
+ s_('SortOptions|Oldest last activity')
+ end
+
+ def sort_title_recently_last_activity
+ s_('SortOptions|Recent last activity')
+ end
+
# Values.
def sort_value_access_level_asc
'access_level_asc'
@@ -445,4 +455,12 @@ module SortingHelper
def sort_value_most_stars
'stars_desc'
end
+
+ def sort_value_oldest_last_activity
+ 'last_activity_on_asc'
+ end
+
+ def sort_value_recently_last_activity
+ 'last_activity_on_desc'
+ end
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 4aba48061ba..5d658d35107 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -13,6 +13,13 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER)
end
+ def render_flash_user_callout(flash_type, message, feature_name)
+ render 'shared/flash_user_callout', flash_type: flash_type, message: message, feature_name: feature_name
+ end
+
+ def render_dashboard_gold_trial(user)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 73c1402eae5..73ca17c6605 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -74,6 +74,15 @@ module UsersHelper
Gitlab.config.gitlab.impersonation_enabled
end
+ def user_badges_in_admin_section(user)
+ [].tap do |badges|
+ badges << { text: s_('AdminUsers|Blocked'), variant: 'danger' } if user.blocked?
+ badges << { text: s_('AdminUsers|Admin'), variant: 'success' } if user.admin?
+ badges << { text: s_('AdminUsers|External'), variant: 'secondary' } if user.external?
+ badges << { text: s_("AdminUsers|It's you!"), variant: nil } if current_user == user
+ end
+ end
+
private
def get_profile_tabs
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index e9fc39e451b..bb5b1555dc4 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -7,8 +7,7 @@ module WorkhorseHelper
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
- headers['Content-Disposition'] = content_disposition(blob, inline)
- headers['Content-Type'] = safe_content_type(blob)
+ headers['Content-Disposition'] = inline ? 'inline' : 'attachment'
# If enabled, this will override the values set above
workhorse_set_content_type!
@@ -47,6 +46,6 @@ module WorkhorseHelper
end
def workhorse_set_content_type!
- headers[Gitlab::Workhorse::DETECT_HEADER] = "true" if Feature.enabled?(:workhorse_set_content_type)
+ headers[Gitlab::Workhorse::DETECT_HEADER] = "true"
end
end
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index e032f568913..e0aa66e6de3 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class AbuseReportMailer < BaseMailer
+ layout 'empty_mailer'
+
+ helper EmailsHelper
+
def notify(abuse_report_id)
return unless deliverable?
diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb
index 45fc5a6c383..d743533b1bc 100644
--- a/app/mailers/email_rejection_mailer.rb
+++ b/app/mailers/email_rejection_mailer.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class EmailRejectionMailer < BaseMailer
+ layout 'empty_mailer'
+
+ helper EmailsHelper
+
def rejection(reason, original_raw, can_retry = false)
@reason = reason
@original_message = Mail::Message.new(original_raw)
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 370e6d2f90b..d2e334fb856 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -74,9 +74,21 @@ module Emails
@new_issue = new_issue
@new_project = new_issue.project
+ @can_access_project = recipient.can?(:read_project, @new_project)
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason))
end
+ def import_issues_csv_email(user_id, project_id, results)
+ @user = User.find(user_id)
+ @project = Project.find(project_id)
+ @results = results
+
+ mail(to: @user.notification_email, subject: subject('Imported issues')) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
+
private
def setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 31e183640ad..fb57c0da34d 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -15,7 +15,7 @@ module Emails
def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project
@pipeline = pipeline
- @merge_request = pipeline.merge_requests.first
+ @merge_request = pipeline.merge_requests_as_head_pipeline.first
add_headers
# We use bcc here because we don't want to generate this emails for a
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 2ac4610967d..80e0a17c312 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -76,6 +76,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id)
end
+ def import_issues_csv_email
+ Notify.import_issues_csv_email(user, project, { success: 3, errors: [5, 6, 7], valid_file: true })
+ end
+
def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 145169be8a6..a24d3476d0e 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -2,6 +2,10 @@
class RepositoryCheckMailer < BaseMailer
# rubocop: disable CodeReuse/ActiveRecord
+ layout 'empty_mailer'
+
+ helper EmailsHelper
+
def notify(failed_count)
@message =
if failed_count == 1
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 0d9c6a4a1f0..1e01f1d17e6 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -5,7 +5,8 @@ class ActiveSession
attr_accessor :created_at, :updated_at,
:session_id, :ip_address,
- :browser, :os, :device_name, :device_type
+ :browser, :os, :device_name, :device_type,
+ :is_impersonated
def current?(session)
return false if session_id.nil? || session.id.nil?
@@ -31,7 +32,8 @@ class ActiveSession
device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp,
- session_id: session_id
+ session_id: session_id,
+ is_impersonated: request.session[:impersonator_id].present?
)
redis.pipelined do
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index bffba3e13fa..bdee9b2b73c 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -8,12 +8,20 @@ class Appearance < ActiveRecord::Base
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
+ cache_markdown_field :header_message, pipeline: :broadcast_message
+ cache_markdown_field :footer_message, pipeline: :broadcast_message
validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte }
+ validates :message_background_color, allow_blank: true, color: true
+ validates :message_font_color, allow_blank: true, color: true
validate :single_appearance_row, on: :create
+ default_value_for :message_background_color, '#E75E40'
+ default_value_for :message_font_color, '#FFFFFF'
+ default_value_for :email_header_and_footer_enabled, false
+
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
mount_uploader :favicon, FaviconUploader
@@ -28,4 +36,44 @@ class Appearance < ActiveRecord::Base
errors.add(:single_appearance_row, 'Only 1 appearances row can exist')
end
end
+
+ def logo_path
+ logo_system_path(logo, 'logo')
+ end
+
+ def header_logo_path
+ logo_system_path(header_logo, 'header_logo')
+ end
+
+ def favicon_path
+ logo_system_path(favicon, 'favicon')
+ end
+
+ def show_header?
+ header_message.present?
+ end
+
+ def show_footer?
+ footer_message.present?
+ end
+
+ private
+
+ def logo_system_path(logo, mount_type)
+ # Legacy attachments may not have have an associated Upload record,
+ # so fallback to the AttachmentUploader#url if this is the
+ # case. AttachmentUploader#path doesn't work because for a local
+ # file, this is an absolute path to the file.
+ return logo&.url unless logo&.upload
+
+ # If we're using a CDN, we need to use the full URL
+ asset_host = ActionController::Base.asset_host
+ local_path = Gitlab::Routing.url_helpers.appearance_upload_path(
+ filename: logo.filename,
+ id: logo.upload.model_id,
+ model: 'appearance',
+ mounted_as: mount_type)
+
+ Gitlab::Utils.append_path(asset_host, local_path)
+ end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 71fbba5b328..a3d662d8250 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -2,4 +2,22 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
+
+ def self.id_in(ids)
+ where(id: ids)
+ end
+
+ def self.safe_find_or_create_by!(*args)
+ safe_find_or_create_by(*args).tap do |record|
+ record.validate! unless record.persisted?
+ end
+ end
+
+ def self.safe_find_or_create_by(*args)
+ transaction(requires_new: true) do
+ find_or_create_by(*args)
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 73be94eade6..c5035797621 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base
include IgnorableColumn
include ChronicDurationAttribute
- add_authentication_token_field :runners_registration_token, encrypted: true, fallback: true
+ add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
@@ -193,6 +193,10 @@ class ApplicationSetting < ActiveRecord::Base
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
+ validates :local_markdown_version,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -246,6 +250,7 @@ class ApplicationSetting < ActiveRecord::Base
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
+ first_day_of_week: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
@@ -303,7 +308,8 @@ class ApplicationSetting < ActiveRecord::Base
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
- protected_ci_variables: false
+ protected_ci_variables: false,
+ local_markdown_version: 0
}
end
@@ -312,7 +318,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
- create(defaults)
+ build_from_defaults.tap(&:save)
end
def self.human_attribute_name(attr, _options = {})
@@ -383,7 +389,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def restricted_visibility_levels=(levels)
- super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
+ super(levels&.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
def strip_sentry_values
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 66a0925c495..c5766eb0327 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -102,7 +102,7 @@ class Blob < SimpleDelegator
# If the blob is a text based blob the content is converted to UTF-8 and any
# invalid byte sequences are replaced.
def data
- if binary?
+ if binary_in_repo?
super
else
@data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
@@ -149,7 +149,7 @@ class Blob < SimpleDelegator
# an LFS pointer, we assume the file stored in LFS is binary, unless a
# text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself.
- def raw_binary?
+ def binary?
if stored_externally?
if rich_viewer
rich_viewer.binary?
@@ -161,7 +161,7 @@ class Blob < SimpleDelegator
true
end
else
- binary?
+ binary_in_repo?
end
end
@@ -180,7 +180,7 @@ class Blob < SimpleDelegator
end
def readable_text?
- text? && !stored_externally? && !truncated?
+ text_in_repo? && !stored_externally? && !truncated?
end
def simple_viewer
@@ -220,7 +220,7 @@ class Blob < SimpleDelegator
def simple_viewer_class
if empty?
BlobViewer::Empty
- elsif raw_binary?
+ elsif binary?
BlobViewer::Download
else # text
BlobViewer::Text
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index eaaf9af1330..df6b9bb2f0b 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -16,7 +16,7 @@ module BlobViewer
def initialize(blob)
@blob = blob
- @initially_binary = blob.binary?
+ @initially_binary = blob.binary_in_repo?
end
def self.partial_path
@@ -52,7 +52,7 @@ module BlobViewer
end
def self.can_render?(blob, verify_binary: true)
- return false if verify_binary && binary? != blob.binary?
+ return false if verify_binary && binary? != blob.binary_in_repo?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(blob.file_type)
@@ -72,7 +72,7 @@ module BlobViewer
end
def binary_detected_after_load?
- !@initially_binary && blob.binary?
+ !@initially_binary && blob.binary_in_repo?
end
# This method is used on the server side to check whether we can attempt to
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index 655241c2808..11228e620c9 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -10,16 +10,16 @@ module BlobViewer
self.file_types = %i(gitlab_ci)
self.binary = false
- def validation_message(project, sha)
+ def validation_message(opts)
return @validation_message if defined?(@validation_message)
prepare!
- @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, { project: project, sha: sha })
+ @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, opts)
end
- def valid?(project, sha)
- validation_message(project, sha).blank?
+ def valid?(opts)
+ validation_message(opts).blank?
end
end
end
diff --git a/app/models/board.rb b/app/models/board.rb
index a137863456c..758a71d6903 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -21,6 +21,10 @@ class Board < ActiveRecord::Base
group_id.present?
end
+ def project_board?
+ project_id.present?
+ end
+
def backlog_list
lists.merge(List.backlog).take
end
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index 92abbb67222..f5b75270595 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -10,7 +10,7 @@ class BoardGroupRecentVisit < ActiveRecord::Base
validates :group, presence: true
validates :board, presence: true
- scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) }
+ scope :by_user_group, -> (user, group) { where(user: user, group: group) }
def self.visited!(user, board)
visit = find_or_create_by(user: user, group: board.group, board: board)
@@ -19,7 +19,10 @@ class BoardGroupRecentVisit < ActiveRecord::Base
retry
end
- def self.latest(user, group)
- by_user_group(user, group).last
+ def self.latest(user, group, count: nil)
+ visits = by_user_group(user, group).order(updated_at: :desc)
+ visits = visits.preload(:board) if count && count > 1
+
+ visits.first(count)
end
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 7cffff906d8..2a1b14b3ae0 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -10,7 +10,7 @@ class BoardProjectRecentVisit < ActiveRecord::Base
validates :project, presence: true
validates :board, presence: true
- scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) }
+ scope :by_user_project, -> (user, project) { where(user: user, project: project) }
def self.visited!(user, board)
visit = find_or_create_by(user: user, project: board.project, board: board)
@@ -19,7 +19,10 @@ class BoardProjectRecentVisit < ActiveRecord::Base
retry
end
- def self.latest(user, project)
- by_user_project(user, project).last
+ def self.latest(user, project, count: nil)
+ visits = by_user_project(user, project).order(updated_at: :desc)
+ visits = visits.preload(:board) if count && count > 1
+
+ visits.first(count)
end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 29aa00a66d9..0d8d7d95791 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -2,13 +2,19 @@
module Ci
class Bridge < CommitStatus
+ include Ci::Processable
+ include Ci::Contextable
include Importable
include AfterCommitQueue
+ include HasRef
include Gitlab::Utils::StrongMemoize
belongs_to :project
+ belongs_to :trigger_request
validates :ref, presence: true
+ delegate :merge_request_event?, to: :pipeline
+
def self.retry(bridge, current_user)
raise NotImplementedError
end
@@ -23,12 +29,31 @@ module Ci
.fabricate!
end
- def predefined_variables
- raise NotImplementedError
+ def schedulable?
+ false
+ end
+
+ def action?
+ false
+ end
+
+ def artifacts?
+ false
+ end
+
+ def runnable?
+ false
+ end
+
+ def expanded_environment_name
end
def execute_hooks
raise NotImplementedError
end
+
+ def to_partial_path
+ 'projects/generic_commit_statuses/generic_commit_status'
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 16a72c680fa..a64c6051f95 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,13 +3,22 @@
module Ci
class Build < CommitStatus
prepend ArtifactMigratable
+ include Ci::Processable
+ include Ci::Metadatable
+ include Ci::Contextable
include TokenAuthenticatable
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include Presentable
include Importable
+ include IgnorableColumn
include Gitlab::Utils::StrongMemoize
include Deployable
+ include HasRef
+
+ BuildArchivedError = Class.new(StandardError)
+
+ ignore_column :commands
belongs_to :project, inverse_of: :builds
belongs_to :runner
@@ -30,25 +39,34 @@ module Ci
has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
end
- has_one :metadata, class_name: 'Ci::BuildMetadata'
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
- delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
+ delegate :merge_request_event?, to: :pipeline
##
- # The "environment" field for builds is a String, and is the unexpanded name!
+ # Since Gitlab 11.5, deployments records started being created right after
+ # `ci_builds` creation. We can look up a relevant `environment` through
+ # `deployment` relation today. This is much more efficient than expanding
+ # environment name with variables.
+ # (See more https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22380)
#
+ # However, we have to still expand environment name if it's a stop action,
+ # because `deployment` persists information for start action only.
+ #
+ # We will follow up this by persisting expanded name in build metadata or
+ # persisting stop action in database.
def persisted_environment
return unless has_environment?
strong_memoize(:persisted_environment) do
- Environment.find_by(name: expanded_environment_name, project: project)
+ deployment&.environment ||
+ Environment.find_by(name: expanded_environment_name, project: project)
end
end
@@ -120,13 +138,12 @@ module Ci
acts_as_taggable
- add_authentication_token_field :token, encrypted: true, fallback: true
+ add_authentication_token_field :token, encrypted: :optional
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
before_destroy { unscoped_project }
- before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -218,8 +235,19 @@ module Ci
before_transition any => [:failed] do |build|
next unless build.project
+ next unless build.deployment
+
+ begin
+ build.deployment.drop!
+ rescue => e
+ Gitlab::Sentry.track_exception(e, extra: { build_id: build.id })
+ end
+
+ true
+ end
- build.deployment&.drop
+ after_transition any => [:failed] do |build|
+ next unless build.project
if build.retry_failure?
begin
@@ -243,10 +271,6 @@ module Ci
end
end
- def ensure_metadata
- metadata || build_metadata(project: project)
- end
-
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
@@ -266,13 +290,8 @@ module Ci
self.name == 'pages'
end
- # degenerated build is one that cannot be run by Runner
- def degenerated?
- self.options.nil?
- end
-
- def degenerate!
- self.update!(options: nil, yaml_variables: nil, commands: nil)
+ def runnable?
+ true
end
def archived?
@@ -384,63 +403,59 @@ module Ci
options&.dig(:environment, :on_stop)
end
- # A slugified version of the build ref, suitable for inclusion in URLs and
- # domain names. Rules:
+ ##
+ # All variables, including persisted environment variables.
#
- # * Lowercased
- # * Anything not matching [a-z0-9-] is replaced with a -
- # * Maximum length is 63 bytes
- # * First/Last Character is not a hyphen
- def ref_slug
- Gitlab::Utils.slugify(ref.to_s)
+ def variables
+ strong_memoize(:variables) do
+ Gitlab::Ci::Variables::Collection.new
+ .concat(persisted_variables)
+ .concat(scoped_variables)
+ .concat(persisted_environment_variables)
+ .to_runner_variables
+ end
end
- ##
- # Variables in the environment name scope.
- #
- def scoped_variables(environment: expanded_environment_name)
+ CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+
+ def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.concat(predefined_variables)
- variables.concat(project.predefined_variables)
- variables.concat(pipeline.predefined_variables)
- variables.concat(runner.predefined_variables) if runner
- variables.concat(project.deployment_variables(environment: environment)) if environment
- variables.concat(yaml_variables)
- variables.concat(user_variables)
- variables.concat(secret_group_variables)
- variables.concat(secret_project_variables(environment: environment))
- variables.concat(trigger_request.user_variables) if trigger_request
- variables.concat(pipeline.variables)
- variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+ break variables unless persisted?
+
+ variables
+ .concat(pipeline.persisted_variables)
+ .append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
+ .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false)
+ .append(key: 'CI_BUILD_ID', value: id.to_s)
+ .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false)
+ .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false)
+ .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
+ .concat(deploy_token_variables)
end
end
- ##
- # Variables that do not depend on the environment name.
- #
- def simple_variables
- strong_memoize(:simple_variables) do
- scoped_variables(environment: nil).to_runner_variables
+ def persisted_environment_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless persisted? && persisted_environment.present?
+
+ variables.concat(persisted_environment.predefined_variables)
+
+ # Here we're passing unexpanded environment_url for runner to expand,
+ # and we need to make sure that CI_ENVIRONMENT_NAME and
+ # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+ variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
- ##
- # All variables, including persisted environment variables.
- #
- def variables
- Gitlab::Ci::Variables::Collection.new
- .concat(persisted_variables)
- .concat(scoped_variables)
- .concat(persisted_environment_variables)
- .to_runner_variables
- end
+ def deploy_token_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless gitlab_deploy_token
- ##
- # Regular Ruby hash of scoped variables, without duplicates that are
- # possible to be present in an array of hashes returned from `variables`.
- #
- def scoped_variables_hash
- scoped_variables.to_hash
+ variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
+ variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
+ end
end
def features
@@ -618,35 +633,6 @@ module Ci
super || project.try(:build_coverage_regex)
end
- def when
- read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
- end
-
- def yaml_variables
- read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || []
- end
-
- def user_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables if user.blank?
-
- variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
- variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
- variables.append(key: 'GITLAB_USER_LOGIN', value: user.username)
- variables.append(key: 'GITLAB_USER_NAME', value: user.name)
- end
- end
-
- def secret_group_variables
- return [] unless project.group
-
- project.group.ci_variables_for(ref, project)
- end
-
- def secret_project_variables(environment: persisted_environment)
- project.ci_variables_for(ref: ref, environment: environment)
- end
-
def steps
[Gitlab::Ci::Build::Step.from_commands(self),
Gitlab::Ci::Build::Step.from_after_script(self)].compact
@@ -749,7 +735,7 @@ module Ci
# Virtual deployment status depending on the environment status.
def deployment_status
- return nil unless starts_environment?
+ return unless starts_environment?
if success?
return successful_deployment_status
@@ -806,89 +792,6 @@ module Ci
@unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
- CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
-
- def persisted_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless persisted?
-
- variables
- .concat(pipeline.persisted_variables)
- .append(key: 'CI_JOB_ID', value: id.to_s)
- .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self))
- .append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false)
- .append(key: 'CI_BUILD_ID', value: id.to_s)
- .append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false)
- .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
- .append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false)
- .append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
- .concat(deploy_token_variables)
- end
- end
-
- def predefined_variables # rubocop:disable Metrics/AbcSize
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI', value: 'true')
- variables.append(key: 'GITLAB_CI', value: 'true')
- variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
- variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
- variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
- variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s)
- variables.append(key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s)
- variables.append(key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s)
- variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
- variables.append(key: 'CI_JOB_NAME', value: name)
- variables.append(key: 'CI_JOB_STAGE', value: stage)
- variables.append(key: 'CI_COMMIT_SHA', value: sha)
- variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
- variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
- variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
- variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
- variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
- variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
- variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
- variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
- variables.concat(legacy_variables)
- end
- end
-
- def legacy_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_REF', value: sha)
- variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
- variables.append(key: 'CI_BUILD_NAME', value: name)
- variables.append(key: 'CI_BUILD_STAGE', value: stage)
- variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
- variables.append(key: "CI_BUILD_TRIGGERED", value: 'true') if trigger_request
- variables.append(key: "CI_BUILD_MANUAL", value: 'true') if action?
- end
- end
-
- def persisted_environment_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless persisted? && persisted_environment.present?
-
- variables.concat(persisted_environment.predefined_variables)
-
- # Here we're passing unexpanded environment_url for runner to expand,
- # and we need to make sure that CI_ENVIRONMENT_NAME and
- # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
- variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
- end
- end
-
- def deploy_token_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- break variables unless gitlab_deploy_token
-
- variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
- variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
- end
- end
-
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
@@ -899,8 +802,11 @@ module Ci
# have the old integer only format. This method returns the retry option
# normalized as a hash in 11.5+ format.
def normalized_retry
- value = options&.dig(:retry)
- value.is_a?(Integer) ? { max: value } : value.to_h
+ strong_memoize(:normalized_retry) do
+ value = options&.dig(:retry)
+ value = value.is_a?(Integer) ? { max: value } : value.to_h
+ value.with_indifferent_access
+ end
end
def build_attributes_from_config
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 9d588b862bd..cd8eb774cf5 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -10,11 +10,15 @@ module Ci
self.table_name = 'ci_builds_metadata'
- belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
+ before_create :set_build_project
+
validates :build, presence: true
- validates :project, presence: true
+
+ serialize :config_options, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :config_variables, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout
@@ -33,5 +37,11 @@ module Ci
update(timeout: timeout, timeout_source: timeout_source)
end
+
+ private
+
+ def set_build_project
+ self.project_id ||= self.build.project_id
+ end
end
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index da08214963f..75017f224a0 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -18,7 +18,7 @@ module Ci
FailedToPersistDataError = Class.new(StandardError)
# Note: The ordering of this enum is related to the precedence of persist store.
- # The bottom item takes the higest precedence, and the top item takes the lowest precedence.
+ # The bottom item takes the highest precedence, and the top item takes the lowest precedence.
enum data_store: {
redis: 1,
database: 2,
@@ -115,7 +115,7 @@ module Ci
current_data = get_data
unless current_data&.bytesize.to_i == CHUNK_SIZE
- raise FailedToPersistDataError, 'Data is not fullfilled in a bucket'
+ raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
end
old_store_class = self.class.get_store_class(data_store)
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 492d1d0329e..323ff560564 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -5,6 +5,7 @@ module Ci
extend Gitlab::Ci::Model
include HasVariable
include Presentable
+ include Maskable
belongs_to :group, class_name: "::Group"
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 11c88200c37..789bb293811 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -73,6 +73,8 @@ module Ci
where(file_type: types)
end
+ scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) }
+
delegate :filename, :exists?, :open, to: :file
enum file_type: {
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 25937065011..ca9725f7a04 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -11,6 +11,11 @@ module Ci
include Gitlab::Utils::StrongMemoize
include AtomicInternalId
include EnumWithNil
+ include HasRef
+ include ShaAttribute
+
+ sha_attribute :source_sha
+ sha_attribute :target_sha
belongs_to :project, inverse_of: :all_pipelines
belongs_to :user
@@ -24,6 +29,8 @@ module Ci
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :processables, -> { processables },
+ class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
@@ -32,7 +39,7 @@ module Ci
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
- has_many :merge_requests, foreign_key: "head_pipeline_id"
+ has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
@@ -44,6 +51,8 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+ has_one :chat_data, class_name: 'Ci::PipelineChatData'
+
accepts_nested_attributes_for :variables, reject_if: :persisted?
delegate :id, to: :project, prefix: true
@@ -51,9 +60,9 @@ module Ci
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
- validates :merge_request, presence: { if: :merge_request? }
- validates :merge_request, absence: { unless: :merge_request? }
- validates :tag, inclusion: { in: [false], if: :merge_request? }
+ validates :merge_request, presence: { if: :merge_request_event? }
+ validates :merge_request, absence: { unless: :merge_request_event? }
+ validates :tag, inclusion: { in: [false], if: :merge_request_event? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
@@ -170,13 +179,38 @@ module Ci
scope :sort_by_merge_request_pipelines, -> do
sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
- query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend
+ query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend
order(query)
end
scope :for_user, -> (user) { where(user: user) }
+ scope :for_merge_request, -> (merge_request, ref, sha) do
+ ##
+ # We have to filter out unrelated MR pipelines.
+ # When merge request is empty, it selects general pipelines, such as push sourced pipelines.
+ # When merge request is matched, it selects MR pipelines.
+ where(merge_request: [nil, merge_request], ref: ref, sha: sha)
+ .sort_by_merge_request_pipelines
+ end
+
+ scope :triggered_by_merge_request, -> (merge_request) do
+ where(source: :merge_request_event, merge_request: merge_request)
+ end
+
+ scope :detached_merge_request_pipelines, -> (merge_request) do
+ triggered_by_merge_request(merge_request).where(target_sha: nil)
+ end
+
+ scope :merge_request_pipelines, -> (merge_request) do
+ triggered_by_merge_request(merge_request).where.not(target_sha: nil)
+ end
+
+ scope :mergeable_merge_request_pipelines, -> (merge_request) do
+ triggered_by_merge_request(merge_request).where(target_sha: merge_request.target_branch_sha)
+ end
+
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
@@ -264,6 +298,10 @@ module Ci
sources.reject { |source| source == "external" }.values
end
+ def self.latest_for_merge_request(merge_request, ref, sha)
+ for_merge_request(merge_request, ref, sha).first
+ end
+
def self.ci_sources_values
config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
end
@@ -299,7 +337,7 @@ module Ci
def ordered_stages
return legacy_stages unless complete?
- if Feature.enabled?('ci_pipeline_persisted_stages')
+ if Feature.enabled?('ci_pipeline_persisted_stages', default_enabled: true)
stages
else
legacy_stages
@@ -379,10 +417,6 @@ module Ci
@commit ||= Commit.lazy(project, sha)
end
- def branch?
- !tag? && !merge_request?
- end
-
def stuck?
pending_builds.any?(&:stuck?)
end
@@ -495,7 +529,7 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
- ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha })
+ ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user })
rescue Gitlab::Ci::YamlProcessor::ValidationError => e
self.yaml_errors = e.message
nil
@@ -580,7 +614,7 @@ module Ci
end
def protected_ref?
- strong_memoize(:protected_ref) { project.protected_for?(ref) }
+ strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
end
def legacy_trigger
@@ -605,7 +639,9 @@ module Ci
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
- if merge_request? && merge_request
+ if merge_request_event? && merge_request
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
variables.concat(merge_request.predefined_variables)
end
end
@@ -633,10 +669,10 @@ module Ci
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||=
- if merge_request?
- project.merge_requests.where(id: merge_request.id)
+ if merge_request_event?
+ MergeRequest.where(id: merge_request_id)
else
- project.merge_requests.where(source_branch: ref)
+ MergeRequest.where(source_project_id: project_id, source_branch: ref)
end
end
@@ -671,9 +707,18 @@ module Ci
end
end
+ # Returns the modified paths.
+ #
+ # The returned value is
+ # * Array: List of modified paths that should be evaluated
+ # * nil: Modified path can not be evaluated
def modified_paths
strong_memoize(:modified_paths) do
- push_details.modified_paths
+ if merge_request_event?
+ merge_request.modified_paths
+ elsif branch_updated?
+ push_details.modified_paths
+ end
end
end
@@ -681,6 +726,22 @@ module Ci
ref == project.default_branch
end
+ def triggered_by_merge_request?
+ merge_request_event? && merge_request_id.present?
+ end
+
+ def detached_merge_request_pipeline?
+ triggered_by_merge_request? && target_sha.nil?
+ end
+
+ def merge_request_pipeline?
+ triggered_by_merge_request? && target_sha.present?
+ end
+
+ def mergeable_merge_request_pipeline?
+ triggered_by_merge_request? && target_sha == merge_request.target_branch_sha
+ end
+
private
def ci_yaml_from_repo
@@ -712,14 +773,16 @@ module Ci
end
def git_ref
- if branch?
- Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
- elsif merge_request?
+ if merge_request_event?
+ ##
+ # In the future, we're going to change this ref to
+ # merge request's merged reference, such as "refs/merge-requests/:iid/merge".
+ # In order to do that, we have to update GitLab-Runner's source pulling
+ # logic.
+ # See https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1092
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
- elsif tag?
- Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
- raise ArgumentError, 'Invalid pipeline type!'
+ super
end
end
diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb
new file mode 100644
index 00000000000..8d37500fec5
--- /dev/null
+++ b/app/models/ci/pipeline_chat_data.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineChatData < ActiveRecord::Base
+ self.table_name = 'ci_pipeline_chat_data'
+
+ belongs_to :chat_name
+
+ validates :pipeline_id, presence: true
+ validates :chat_name_id, presence: true
+ validates :response_url, presence: true
+ end
+end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 2994aaae4aa..571c4271475 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -22,7 +22,8 @@ module Ci
schedule: 4,
api: 5,
external: 6,
- merge_request: 10
+ chat: 8,
+ merge_request_event: 10
}
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 8249199e76f..ce26ee168ef 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -10,7 +10,7 @@ module Ci
include FromUnion
include TokenAuthenticatable
- add_authentication_token_field :token, encrypted: true, migrating: true
+ add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption) ? :optional : :required }
enum access_level: {
not_protected: 0,
@@ -97,6 +97,7 @@ module Ci
scope :order_contacted_at_asc, -> { order(contacted_at: :asc) }
scope :order_created_at_desc, -> { order(created_at: :desc) }
+ scope :with_tags, -> { preload(:tags) }
validate :tag_constraints
validates :access_level, presence: true
@@ -256,6 +257,10 @@ module Ci
end
end
+ def uncached_contacted_at
+ read_attribute(:contacted_at)
+ end
+
private
def cleanup_runner_queue
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 58f3fe2460a..0389945191e 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -14,6 +14,7 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
+ has_many :bridges, foreign_key: :stage_id
with_options unless: :importing? do
validates :project, presence: true
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 55db42162ca..637148c4ce4 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,6 +4,7 @@ module Ci
class Trigger < ActiveRecord::Base
extend Gitlab::Ci::Model
include IgnorableColumn
+ include Presentable
ignore_column :deleted_at
@@ -29,7 +30,7 @@ module Ci
end
def short_token
- token[0...4]
+ token[0...4] if token.present?
end
def legacy?
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 524d79014f8..64836ea4fa4 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -5,6 +5,7 @@ module Ci
extend Gitlab::Ci::Model
include HasVariable
include Presentable
+ include Maskable
belongs_to :project
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 74ef7c7e145..c758577815a 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class CertManager < ActiveRecord::Base
- VERSION = 'v0.5.0'.freeze
+ VERSION = 'v0.5.2'.freeze
self.table_name = 'clusters_applications_cert_managers'
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 8f8790585a3..7c15aaa4825 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Ingress < ActiveRecord::Base
- VERSION = '0.23.0'.freeze
+ VERSION = '1.1.2'.freeze
self.table_name = 'clusters_applications_ingress'
@@ -23,7 +23,7 @@ module Clusters
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
- before_transition any => [:installed] do |application|
+ after_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 0c72d7d8340..8d79b041b64 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -5,7 +5,7 @@ module Clusters
class Knative < ActiveRecord::Base
VERSION = '0.2.2'.freeze
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
-
+ METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze
FETCH_IP_ADDRESS_DELAY = 30.seconds
self.table_name = 'clusters_applications_knative'
@@ -19,8 +19,15 @@ module Clusters
self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
+ def set_initial_status
+ return unless not_installable?
+ return unless verify_cluster?
+
+ self.status = 'installable'
+ end
+
state_machine :status do
- before_transition any => [:installed] do |application|
+ after_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
@@ -34,6 +41,8 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
+ after_save :clear_reactive_cache!
+
def chart
'knative/knative'
end
@@ -49,7 +58,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: REPOSITORY
+ repository: REPOSITORY,
+ postinstall: install_knative_metrics
)
end
@@ -71,7 +81,7 @@ module Clusters
end
def calculate_reactive_cache
- { services: read_services }
+ { services: read_services, pods: read_pods }
end
def ingress_service
@@ -79,7 +89,7 @@ module Clusters
end
def services_for(ns: namespace)
- return unless services
+ return [] unless services
return [] unless ns
services.select do |service|
@@ -87,13 +97,35 @@ module Clusters
end
end
+ def service_pod_details(ns, service)
+ with_reactive_cache do |data|
+ data[:pods].select { |pod| filter_pods(pod, ns, service) }
+ end
+ end
+
private
+ def read_pods
+ cluster.kubeclient.core_client.get_pods.as_json
+ end
+
+ def filter_pods(pod, namespace, service)
+ pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+ end
+
def read_services
client.get_services.as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
+
+ def install_knative_metrics
+ ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
+ end
+
+ def verify_cluster?
+ cluster&.application_helm_available? && cluster&.platform_kubernetes_rbac?
+ end
end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 46d0388a464..fa7ce363531 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -5,7 +5,7 @@ module Clusters
class Prometheus < ActiveRecord::Base
include PrometheusAdapter
- VERSION = '6.7.3'.freeze
+ VERSION = '6.7.3'
self.table_name = 'clusters_applications_prometheus'
@@ -24,14 +24,6 @@ module Clusters
end
end
- def ready_status
- [:installed]
- end
-
- def ready?
- ready_status.include?(status_name)
- end
-
def chart
'stable/prometheus'
end
@@ -50,10 +42,29 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files
+ files: files,
+ postinstall: install_knative_metrics
)
end
+ def upgrade_command(values)
+ ::Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: name,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files_with_replaced_values(values)
+ )
+ end
+
+ # Returns a copy of files where the values of 'values.yaml'
+ # are replaced by the argument.
+ #
+ # See #values for the data format required
+ def files_with_replaced_values(replaced_values)
+ files.merge('values.yaml': replaced_values)
+ end
+
def prometheus_client
return unless kube_client
@@ -74,6 +85,10 @@ module Clusters
def kube_client
cluster&.kubeclient&.core_client
end
+
+ def install_knative_metrics
+ ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available?
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index c931b340b24..941551dadaa 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.39'.freeze
+ VERSION = '0.2.0'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7fe43cd2de0..be3e6a05e1e 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -18,6 +18,7 @@ module Clusters
Applications::Knative.application_name => Applications::Knative
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
+ KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze
belongs_to :user
@@ -49,8 +50,9 @@ module Clusters
validates :name, cluster_name: true
validates :cluster_type, presence: true
- validate :restrict_modification, on: :update
+ validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type?
validate :no_projects, unless: :project_type?
@@ -63,6 +65,10 @@ module Clusters
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
+ delegate :available?, to: :application_knative, prefix: true, allow_nil: true
+ delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
+
+ alias_attribute :base_domain, :domain
enum cluster_type: {
instance_type: 1,
@@ -93,7 +99,7 @@ module Clusters
where('NOT EXISTS (?)', subquery)
end
- scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.installed) }
+ scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.available) }
scope :preload_knative, -> {
preload(
@@ -191,8 +197,42 @@ module Clusters
project_type?
end
+ def kube_ingress_domain
+ @kube_ingress_domain ||= domain.presence || instance_domain || legacy_auto_devops_domain
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless kube_ingress_domain
+
+ variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain)
+ end
+ end
+
private
+ def instance_domain
+ @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain
+ end
+
+ # To keep backward compatibility with AUTO_DEVOPS_DOMAIN
+ # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
+ # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:
+ # ProjectAutoDevops#Domain, project variables or group variables,
+ # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL
+ #
+ # This method should is scheduled to be removed on
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/56959
+ def legacy_auto_devops_domain
+ if project_type?
+ project&.auto_devops&.domain.presence ||
+ project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence ||
+ project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
+ elsif group_type?
+ group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence
+ end
+ end
+
def restrict_modification
if provider&.on_creation?
errors.add(:base, "cannot modify during creation")
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 0e74cce29b7..1273ed83abe 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -6,7 +6,14 @@ module Clusters
extend ActiveSupport::Concern
included do
- scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
+ scope :available, -> do
+ where(
+ status: [
+ self.state_machines[:status].states[:installed].value,
+ self.state_machines[:status].states[:updated].value
+ ]
+ )
+ end
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
@@ -20,7 +27,7 @@ module Clusters
state :update_errored, value: 6
event :make_scheduled do
- transition [:installable, :errored] => :scheduled
+ transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled
end
event :make_installing do
@@ -29,18 +36,16 @@ module Clusters
event :make_installed do
transition [:installing] => :installed
+ transition [:updating] => :updated
end
event :make_errored do
- transition any => :errored
+ transition any - [:updating] => :errored
+ transition [:updating] => :update_errored
end
event :make_updating do
- transition [:installed, :updated, :update_errored] => :updating
- end
-
- event :make_updated do
- transition [:updating] => :updated
+ transition [:installed, :updated, :update_errored, :scheduled] => :updating
end
event :make_update_errored do
@@ -74,9 +79,17 @@ module Clusters
end
end
+ def updateable?
+ installed? || updated? || update_errored?
+ end
+
def available?
installed? || updated?
end
+
+ def update_in_progress?
+ updating?
+ end
end
end
end
diff --git a/app/models/clusters/concerns/application_version.rb b/app/models/clusters/concerns/application_version.rb
index ccad74dc35a..db94e8e08c9 100644
--- a/app/models/clusters/concerns/application_version.rb
+++ b/app/models/clusters/concerns/application_version.rb
@@ -7,11 +7,15 @@ module Clusters
included do
state_machine :status do
- after_transition any => [:installing] do |application|
- application.update(version: application.class.const_get(:VERSION))
+ before_transition any => [:installed, :updated] do |application|
+ application.version = application.class.const_get(:VERSION)
end
end
end
+
+ def update_available?
+ version != self.class.const_get(:VERSION)
+ end
end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 6cec497b4e4..814fc591408 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -41,8 +41,9 @@ module Clusters
validate :no_namespace, unless: :allow_user_defined_namespace?
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
- validates :api_url, url: true, presence: true
+ validates :api_url, public_url: true, presence: true
validates :token, presence: true
+ validates :ca_cert, certificate: true, allow_blank: true, if: :ca_cert_changed?
validate :prevent_modification, on: :update
@@ -65,6 +66,8 @@ module Clusters
abac: 2
}
+ default_value_for :authorization_type, :rbac
+
def actual_namespace
if namespace.present?
namespace
@@ -96,6 +99,8 @@ module Clusters
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
.append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
+
+ variables.concat(cluster.predefined_variables)
end
end
@@ -106,7 +111,7 @@ module Clusters
def terminals(environment)
with_reactive_cache do |data|
pods = filter_by_label(data[:pods], app: environment.slug)
- terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
+ terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.compact
terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -152,7 +157,7 @@ module Clusters
def build_kube_client!
raise "Incomplete settings" unless api_url
- raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
+ raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
diff --git a/app/models/commit.rb b/app/models/commit.rb
index a422a0995ff..f412d252e5c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -11,6 +11,7 @@ class Commit
include Mentionable
include Referable
include StaticModel
+ include Presentable
include ::Gitlab::Utils::StrongMemoize
attr_mentionable :safe_message, pipeline: :single_line
@@ -304,7 +305,9 @@ class Commit
end
def last_pipeline
- @last_pipeline ||= pipelines.last
+ strong_memoize(:last_pipeline) do
+ pipelines.last
+ end
end
def status(ref = nil)
@@ -376,7 +379,7 @@ class Commit
end
def merge_commit?
- parents.size > 1
+ parent_ids.size > 1
end
def merged_merge_request(current_user)
@@ -469,6 +472,10 @@ class Commit
!!merged_merge_request(user)
end
+ def cache_key
+ "commit:#{sha}"
+ end
+
private
def commit_reference(from, referable_commit_id, full: false)
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index e349f0fe971..a9a2e9c81eb 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -3,6 +3,7 @@
# A collection of Commit instances for a specific project and Git reference.
class CommitCollection
include Enumerable
+ include Gitlab::Utils::StrongMemoize
attr_reader :project, :ref, :commits
@@ -19,6 +20,18 @@ class CommitCollection
commits.each(&block)
end
+ def authors
+ emails = without_merge_commits.map(&:author_email).uniq
+
+ User.by_any_email(emails)
+ end
+
+ def without_merge_commits
+ strong_memoize(:without_merge_commits) do
+ commits.reject(&:merge_commit?)
+ end
+ end
+
# Sets the pipeline status for every commit.
#
# Setting this status ahead of time removes the need for running a query for
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 094747ee48d..920b1d092dd 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -134,25 +134,25 @@ class CommitRange
end
def sha_from
- return nil unless @commit_from
+ return unless @commit_from
@commit_from.id
end
def sha_to
- return nil unless @commit_to
+ return unless @commit_to
@commit_to.id
end
def sha_start
- return nil unless sha_from
+ return unless sha_from
exclude_start? ? sha_from + '^' : sha_from
end
def commit_start
- return nil unless sha_start
+ return unless sha_start
if exclude_start?
@commit_start ||= project.commit(sha_start)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 0f50bd39131..7f6562b63e5 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -41,6 +41,7 @@ class CommitStatus < ActiveRecord::Base
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
+ scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) }
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
diff --git a/app/models/concerns/blob_language_from_git_attributes.rb b/app/models/concerns/blob_language_from_git_attributes.rb
index 70213d22147..56e1276a220 100644
--- a/app/models/concerns/blob_language_from_git_attributes.rb
+++ b/app/models/concerns/blob_language_from_git_attributes.rb
@@ -5,7 +5,7 @@ module BlobLanguageFromGitAttributes
extend ActiveSupport::Concern
def language_from_gitattributes
- return nil unless project
+ return unless project
repository = project.repository
repository.gitattribute(path, 'gitlab-language')
diff --git a/app/models/concerns/blob_like.rb b/app/models/concerns/blob_like.rb
index f20f01486a5..dc80f8d62f4 100644
--- a/app/models/concerns/blob_like.rb
+++ b/app/models/concerns/blob_like.rb
@@ -28,7 +28,7 @@ module BlobLike
nil
end
- def binary?
+ def binary_in_repo?
false
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index a8c9e54f00c..1a8570b80c3 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -13,9 +13,8 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 12
+ CACHE_COMMONMARK_VERSION = 14
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
@@ -42,18 +41,6 @@ module CacheMarkdownField
end
end
- class MarkdownEngine
- def self.from_version(version = nil)
- return :common_mark if version.nil? || version == 0
-
- if version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
- :redcarpet
- else
- :common_mark
- end
- end
- end
-
def skip_project_check?
false
end
@@ -71,7 +58,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = MarkdownEngine.from_version(latest_cached_markdown_version)
+ context[:markdown_engine] = :common_mark
context
end
@@ -128,12 +115,27 @@ module CacheMarkdownField
end
def latest_cached_markdown_version
- return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version
+ @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version
+ end
- if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
- CacheMarkdownField::CACHE_REDCARPET_VERSION
+ def local_version
+ # because local_markdown_version is stored in application_settings which
+ # uses cached_markdown_version too, we check explicitly to avoid
+ # endless loop
+ return local_markdown_version if has_attribute?(:local_markdown_version)
+
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ # Following migrations are not properly isolated and
+ # use real models (by calling .ghost method), in these migrations
+ # local_markdown_version attribute doesn't exist yet, so we
+ # use a default value:
+ # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
+ # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
+ if settings.respond_to?(:local_markdown_version)
+ settings.local_markdown_version
else
- CacheMarkdownField::CACHE_COMMONMARK_VERSION
+ 0
end
end
@@ -178,7 +180,9 @@ module CacheMarkdownField
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
- invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
+ invalidations.delete(markdown_field.to_s) if changed_fields.include?("#{markdown_field}_html")
+
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
end
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index 75592bb63e2..3d60f6924c1 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -23,7 +23,12 @@ module CacheableAttributes
end
def build_from_defaults(attributes = {})
- new(defaults.merge(attributes))
+ final_attributes = defaults
+ .merge(attributes)
+ .stringify_keys
+ .slice(*column_names)
+
+ new(final_attributes)
end
def cached
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
new file mode 100644
index 00000000000..4986a42dbd2
--- /dev/null
+++ b/app/models/concerns/ci/contextable.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that provide context in form of
+ # essential CI/CD variables that can be used by a build / bridge job.
+ #
+ module Contextable
+ ##
+ # Variables in the environment name scope.
+ #
+ def scoped_variables(environment: expanded_environment_name)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.concat(predefined_variables)
+ variables.concat(project.predefined_variables)
+ variables.concat(pipeline.predefined_variables)
+ variables.concat(runner.predefined_variables) if runnable? && runner
+ variables.concat(project.deployment_variables(environment: environment)) if environment
+ variables.concat(yaml_variables)
+ variables.concat(user_variables)
+ variables.concat(secret_group_variables)
+ variables.concat(secret_project_variables(environment: environment))
+ variables.concat(trigger_request.user_variables) if trigger_request
+ variables.concat(pipeline.variables)
+ variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+ end
+ end
+
+ ##
+ # Regular Ruby hash of scoped variables, without duplicates that are
+ # possible to be present in an array of hashes returned from `variables`.
+ #
+ def scoped_variables_hash
+ scoped_variables.to_hash
+ end
+
+ ##
+ # Variables that do not depend on the environment name.
+ #
+ def simple_variables
+ strong_memoize(:simple_variables) do
+ scoped_variables(environment: nil).to_runner_variables
+ end
+ end
+
+ def user_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables if user.blank?
+
+ variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
+ variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
+ variables.append(key: 'GITLAB_USER_LOGIN', value: user.username)
+ variables.append(key: 'GITLAB_USER_NAME', value: user.name)
+ end
+ end
+
+ def predefined_variables # rubocop:disable Metrics/AbcSize
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI', value: 'true')
+ variables.append(key: 'GITLAB_CI', value: 'true')
+ variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(','))
+ variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
+ variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
+ variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s)
+ variables.append(key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s)
+ variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision)
+ variables.append(key: 'CI_JOB_NAME', value: name)
+ variables.append(key: 'CI_JOB_STAGE', value: stage)
+ variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
+ variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
+ variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
+ variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
+ variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
+ variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
+ variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
+ variables.concat(legacy_variables)
+ end
+ end
+
+ def legacy_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_BUILD_REF', value: sha)
+ variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_BUILD_NAME', value: name)
+ variables.append(key: 'CI_BUILD_STAGE', value: stage)
+ variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
+ variables.append(key: "CI_BUILD_TRIGGERED", value: 'true') if trigger_request
+ variables.append(key: "CI_BUILD_MANUAL", value: 'true') if action?
+ end
+ end
+
+ def secret_group_variables
+ return [] unless project.group
+
+ project.group.ci_variables_for(git_ref, project)
+ end
+
+ def secret_project_variables(environment: persisted_environment)
+ project.ci_variables_for(ref: git_ref, environment: environment)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
new file mode 100644
index 00000000000..9eed9492b37
--- /dev/null
+++ b/app/models/concerns/ci/metadatable.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that need to read and write
+ # metadata for CI/CD entities.
+ #
+ module Metadatable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :metadata, class_name: 'Ci::BuildMetadata',
+ foreign_key: :build_id,
+ inverse_of: :build,
+ autosave: true
+
+ delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ before_create :ensure_metadata
+ end
+
+ def ensure_metadata
+ metadata || build_metadata(project: project)
+ end
+
+ def degenerated?
+ self.options.blank?
+ end
+
+ def degenerate!
+ self.class.transaction do
+ self.update!(options: nil, yaml_variables: nil)
+ self.metadata&.destroy
+ end
+ end
+
+ def options
+ read_metadata_attribute(:options, :config_options, {})
+ end
+
+ def yaml_variables
+ read_metadata_attribute(:yaml_variables, :config_variables, [])
+ end
+
+ def options=(value)
+ write_metadata_attribute(:options, :config_options, value)
+ end
+
+ def yaml_variables=(value)
+ write_metadata_attribute(:yaml_variables, :config_variables, value)
+ end
+
+ private
+
+ def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
+ read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value
+ end
+
+ def write_metadata_attribute(legacy_key, metadata_key, value)
+ # save to metadata or this model depending on the state of feature flag
+ if Feature.enabled?(:ci_build_metadata_config)
+ ensure_metadata.write_attribute(metadata_key, value)
+ write_attribute(legacy_key, nil)
+ else
+ write_attribute(legacy_key, value)
+ metadata&.write_attribute(metadata_key, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb
new file mode 100644
index 00000000000..268fa8ec692
--- /dev/null
+++ b/app/models/concerns/ci/processable.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that need to be implemented by CI/CD
+ # entities that are supposed to go through pipeline processing
+ # services.
+ #
+ #
+ module Processable
+ def schedulable?
+ raise NotImplementedError
+ end
+
+ def action?
+ raise NotImplementedError
+ end
+
+ def when
+ read_attribute(:when) || 'on_success'
+ end
+
+ def expanded_environment_name
+ raise NotImplementedError
+ end
+
+ def scoped_variables_hash
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/concerns/closed_at_filterable.rb b/app/models/concerns/closed_at_filterable.rb
new file mode 100644
index 00000000000..239c2e47611
--- /dev/null
+++ b/app/models/concerns/closed_at_filterable.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ClosedAtFilterable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :closed_before, ->(date) { where(scoped_table[:closed_at].lteq(date)) }
+ scope :closed_after, ->(date) { where(scoped_table[:closed_at].gteq(date)) }
+
+ def self.scoped_table
+ arel_table.alias(table_name)
+ end
+ end
+end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 86b61248534..e4e5928f5cf 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -9,7 +9,7 @@ module DiscussionOnDiff
included do
delegate :line_code,
:original_line_code,
- :diff_file,
+ :note_diff_file,
:diff_line,
:active?,
:created_at_diff?,
@@ -60,6 +60,13 @@ module DiscussionOnDiff
prev_lines
end
+ def diff_file
+ strong_memoize(:diff_file) do
+ # Falling back here is important as `note_diff_files` are created async.
+ fetch_preloaded_diff_file || first_note.diff_file
+ end
+ end
+
def line_code_in_diffs(diff_refs)
if active?(diff_refs)
line_code
@@ -67,4 +74,15 @@ module DiscussionOnDiff
original_line_code
end
end
+
+ private
+
+ def fetch_preloaded_diff_file
+ fetch_preloaded_diff =
+ context_noteable &&
+ context_noteable.preloads_discussion_diff_highlighting? &&
+ note_diff_file
+
+ context_noteable.discussions_diffs.find_by_id(note_diff_file.id) if fetch_preloaded_diff
+ end
end
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index 1e3afd641ed..f862031bce0 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -11,7 +11,7 @@
# it is difficult to accomplish it.
#
# This module defines a format to use `delete_all` and delete associated external data.
-# Here is an exmaple
+# Here is an example
#
# Situation
# - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build`
diff --git a/app/models/concerns/feature_gate.rb b/app/models/concerns/feature_gate.rb
index 3f84de54ad5..bb095f113e2 100644
--- a/app/models/concerns/feature_gate.rb
+++ b/app/models/concerns/feature_gate.rb
@@ -2,7 +2,7 @@
module FeatureGate
def flipper_id
- return nil if new_record?
+ return if new_record?
"#{self.class.name}:#{id}"
end
diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb
new file mode 100644
index 00000000000..413cd36dcaa
--- /dev/null
+++ b/app/models/concerns/has_ref.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module HasRef
+ extend ActiveSupport::Concern
+
+ def branch?
+ !tag? && !merge_request_event?
+ end
+
+ def git_ref
+ if branch?
+ Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
+ elsif tag?
+ Gitlab::Git::TAG_REF_PREFIX + ref.to_s
+ end
+ end
+
+ # A slugified version of the build ref, suitable for inclusion in URLs and
+ # domain names. Rules:
+ #
+ # * Lowercased
+ # * Anything not matching [a-z0-9-] is replaced with a -
+ # * Maximum length is 63 bytes
+ # * First/Last Character is not a hyphen
+ def ref_slug
+ Gitlab::Utils.slugify(ref.to_s)
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index b92643f87f8..0d2be4c61ab 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -85,11 +85,11 @@ module HasStatus
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
- scope :failed, -> { where(status: 'failed') }
- scope :canceled, -> { where(status: 'canceled') }
- scope :skipped, -> { where(status: 'skipped') }
- scope :manual, -> { where(status: 'manual') }
- scope :scheduled, -> { where(status: 'scheduled') }
+ scope :failed, -> { where(status: 'failed') }
+ scope :canceled, -> { where(status: 'canceled') }
+ scope :skipped, -> { where(status: 'skipped') }
+ scope :manual, -> { where(status: 'manual') }
+ scope :scheduled, -> { where(status: 'scheduled') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
index dfbe413a878..2ec42a1029b 100644
--- a/app/models/concerns/has_variable.rb
+++ b/app/models/concerns/has_variable.rb
@@ -21,9 +21,9 @@ module HasVariable
def key=(new_key)
super(new_key.to_s.strip)
end
+ end
- def to_runner_variable
- { key: key, value: value, public: false }
- end
+ def to_runner_variable
+ { key: key, value: value, public: false }
end
end
diff --git a/app/models/concerns/iid_routes.rb b/app/models/concerns/iid_routes.rb
index b7f99e845ca..3eeb29b6595 100644
--- a/app/models/concerns/iid_routes.rb
+++ b/app/models/concerns/iid_routes.rb
@@ -4,7 +4,7 @@ module IidRoutes
##
# This automagically enforces all related routes to use `iid` instead of `id`
# If you want to use `iid` for some routes and `id` for other routes, this module should not to be included,
- # instead you should define `iid` or `id` explictly at each route generators. e.g. pipeline_path(project.id, pipeline.iid)
+ # instead you should define `iid` or `id` explicitly at each route generators. e.g. pipeline_path(project.id, pipeline.iid)
def to_param
iid.to_s
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 0d363ec68b7..c7ad182ab82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -23,11 +23,12 @@ module Issuable
include Sortable
include CreatedAtFilterable
include UpdatedAtFilterable
+ include ClosedAtFilterable
# This object is used to gather issuable meta data for displaying
# 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, :merge_requests_count)
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :user_notes_count, :merge_requests_count)
included do
cache_markdown_field :title, pipeline: :single_line
@@ -35,8 +36,8 @@ module Issuable
redact_field :description
- belongs_to :author, class_name: "User"
- belongs_to :updated_by, class_name: "User"
+ belongs_to :author, class_name: 'User'
+ belongs_to :updated_by, class_name: 'User'
belongs_to :last_edited_by, class_name: 'User'
belongs_to :milestone
@@ -74,6 +75,7 @@ module Issuable
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
+ validate :milestone_is_valid
scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
@@ -117,6 +119,16 @@ module Issuable
def has_multiple_assignees?
assignees.count > 1
end
+
+ def milestone_available?
+ project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
+ end
+
+ private
+
+ def milestone_is_valid
+ errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available?
+ end
end
class_methods do
@@ -136,10 +148,18 @@ module Issuable
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
+ # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma.
#
# Returns an ActiveRecord::Relation.
- def full_search(query)
- fuzzy_search(query, [:title, :description])
+ def full_search(query, matched_columns: 'title,description')
+ allowed_columns = [:title, :description]
+ matched_columns = matched_columns.to_s.split(',').map(&:to_sym)
+ matched_columns &= allowed_columns
+
+ # Matching title or description if the matched_columns did not contain any allowed columns.
+ matched_columns = [:title, :description] if matched_columns.empty?
+
+ fuzzy_search(query, matched_columns)
end
def sort_by_attribute(method, excluded_labels: [])
@@ -270,26 +290,29 @@ module Issuable
def to_hook_data(user, old_associations: {})
changes = previous_changes
- old_labels = old_associations.fetch(:labels, [])
- old_assignees = old_associations.fetch(:assignees, [])
- if old_labels != labels
- changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
- end
+ if old_associations
+ old_labels = old_associations.fetch(:labels, [])
+ old_assignees = old_associations.fetch(:assignees, [])
- if old_assignees != assignees
- if self.is_a?(Issue)
- changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
- else
- changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
+ if old_labels != labels
+ changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
end
- end
- if self.respond_to?(:total_time_spent)
- old_total_time_spent = old_associations.fetch(:total_time_spent, nil)
+ if old_assignees != assignees
+ if self.is_a?(Issue)
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
+ else
+ changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
+ end
+ end
+
+ if self.respond_to?(:total_time_spent)
+ old_total_time_spent = old_associations.fetch(:total_time_spent, nil)
- if old_total_time_spent != total_time_spent
- changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ if old_total_time_spent != total_time_spent
+ changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ end
end
end
diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb
index e18edd33ba7..ff61412767e 100644
--- a/app/models/concerns/manual_inverse_association.rb
+++ b/app/models/concerns/manual_inverse_association.rb
@@ -5,8 +5,8 @@ module ManualInverseAssociation
class_methods do
def manual_inverse_association(association, inverse)
- define_method(association) do |*args|
- super(*args).tap do |value|
+ define_method(association) do
+ super().tap do |value|
next unless value
child_association = value.association(inverse)
diff --git a/app/models/concerns/maskable.rb b/app/models/concerns/maskable.rb
new file mode 100644
index 00000000000..8793f0ec965
--- /dev/null
+++ b/app/models/concerns/maskable.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Maskable
+ extend ActiveSupport::Concern
+
+ # * Single line
+ # * No escape characters
+ # * No variables
+ # * No spaces
+ # * Minimal length of 8 characters
+ # * Absolutely no fun is allowed
+ REGEX = /\A\w{8,}\z/
+
+ included do
+ validates :masked, inclusion: { in: [true, false] }
+ validates :value, format: { with: REGEX }, if: :masked?
+ end
+
+ def to_runner_variable
+ super.merge(masked: masked?)
+ end
+end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e44a069b730..e65bbb8ca07 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -42,16 +42,35 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
IssuesFinder.new(user, issues_finder_params)
- .execute.preload(:assignees).where(milestone_id: milestoneish_ids)
+ .execute.preload(:assignees).where(milestone_id: milestoneish_id)
end
end
+ def issue_participants_visible_by_user(user)
+ User.joins(:issue_assignees)
+ .where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id))
+ .distinct
+ end
+
+ def issue_labels_visible_by_user(user)
+ Label.joins(:label_links)
+ .where('label_links.target_id' => issues_visible_to_user(user).select(:id), 'label_links.target_type' => 'Issue')
+ .distinct
+ end
+
def sorted_issues(user)
issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
end
- def sorted_merge_requests
- merge_requests.sort_by_attribute('label_priority')
+ def sorted_merge_requests(user)
+ merge_requests_visible_to_user(user).sort_by_attribute('label_priority')
+ end
+
+ def merge_requests_visible_to_user(user)
+ memoize_per_user(user, :merge_requests_visible_to_user) do
+ MergeRequestsFinder.new(user, issues_finder_params)
+ .execute.where(milestone_id: milestoneish_id)
+ end
end
def upcoming?
diff --git a/app/models/concerns/mirror_authentication.rb b/app/models/concerns/mirror_authentication.rb
index e3e1a0441f8..948094221e5 100644
--- a/app/models/concerns/mirror_authentication.rb
+++ b/app/models/concerns/mirror_authentication.rb
@@ -79,7 +79,7 @@ module MirrorAuthentication
end
def ssh_public_key
- return nil if ssh_private_key.blank?
+ return if ssh_private_key.blank?
comment = "git@#{::Gitlab.config.gitlab.host}"
::SSHKey.new(ssh_private_key, comment: comment).ssh_public_key
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f2cad09e779..3c74034b527 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -1,9 +1,18 @@
# frozen_string_literal: true
module Noteable
- # Names of all implementers of `Noteable` that support resolvable notes.
+ extend ActiveSupport::Concern
+
+ # `Noteable` class names that support resolvable notes.
RESOLVABLE_TYPES = %w(MergeRequest).freeze
+ class_methods do
+ # `Noteable` class names that support replying to individual notes.
+ def replyable_types
+ %w(Issue MergeRequest)
+ end
+ end
+
def base_class_name
self.class.base_class.name
end
@@ -26,6 +35,10 @@ module Noteable
DiscussionNote.noteable_types.include?(base_class_name)
end
+ def supports_replying_to_individual_notes?
+ supports_discussions? && self.class.replyable_types.include?(base_class_name)
+ end
+
def supports_suggestion?
false
end
@@ -34,6 +47,10 @@ module Noteable
false
end
+ def preloads_discussion_diff_highlighting?
+ false
+ end
+
def discussion_notes
notes
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index d3572875fb3..d2ead7130e5 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -69,14 +69,14 @@ module ReactiveCaching
def with_reactive_cache(*args, &blk)
unless within_reactive_cache_lifetime?(*args)
refresh_reactive_cache!(*args)
- return nil
+ return
end
keep_alive_reactive_cache!(*args)
begin
data = Rails.cache.read(full_reactive_cache_key(*args))
- yield data if data.present?
+ yield data unless data.nil?
rescue InvalidateReactiveCache
refresh_reactive_cache!(*args)
nil
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index e51b4e22c96..a479bef993c 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -16,6 +16,8 @@ module ShaAttribute
# the column is the correct type. In production it should behave like any other attribute.
# See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5502 for more discussion
def validate_binary_column_exists!(name)
+ return unless database_exists?
+
unless table_exists?
warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
return
@@ -35,5 +37,13 @@ module ShaAttribute
Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
raise
end
+
+ def database_exists?
+ ActiveRecord::Base.connection
+
+ true
+ rescue
+ false
+ end
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 603d4d62578..f147ce8ad6b 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -9,9 +9,11 @@ require 'task_list/filter'
#
# Used by MergeRequest and Issue
module Taskable
- COMPLETED = 'completed'.freeze
- INCOMPLETE = 'incomplete'.freeze
- ITEM_PATTERN = %r{
+ COMPLETED = 'completed'.freeze
+ INCOMPLETE = 'incomplete'.freeze
+ COMPLETE_PATTERN = /(\[[xX]\])/.freeze
+ INCOMPLETE_PATTERN = /(\[[\s]\])/.freeze
+ ITEM_PATTERN = %r{
^
\s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list
\s+ # whitespace prefix has to be always presented for a list item
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index 01fb194281a..df14e6e4754 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -39,22 +39,6 @@ module TokenAuthenticatableStrategies
instance.save! if Gitlab::Database.read_write?
end
- def fallback?
- unless options[:fallback].in?([true, false, nil])
- raise ArgumentError, 'fallback: needs to be a boolean value!'
- end
-
- options[:fallback] == true
- end
-
- def migrating?
- unless options[:migrating].in?([true, false, nil])
- raise ArgumentError, 'migrating: needs to be a boolean value!'
- end
-
- options[:migrating] == true
- end
-
def self.fabricate(model, field, options)
if options[:digest] && options[:encrypted]
raise ArgumentError, 'Incompatible options set!'
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
index 152491aa6e9..2c7fa2c5b3c 100644
--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -2,28 +2,18 @@
module TokenAuthenticatableStrategies
class Encrypted < Base
- def initialize(*)
- super
-
- if migrating? && fallback?
- raise ArgumentError, '`fallback` and `migrating` options are not compatible!'
- end
- end
-
def find_token_authenticatable(token, unscoped = false)
return if token.blank?
- if fully_encrypted?
- return find_by_encrypted_token(token, unscoped)
- end
-
- if fallback?
+ if required?
+ find_by_encrypted_token(token, unscoped)
+ elsif optional?
find_by_encrypted_token(token, unscoped) ||
find_by_plaintext_token(token, unscoped)
elsif migrating?
find_by_plaintext_token(token, unscoped)
else
- raise ArgumentError, 'Unknown encryption phase!'
+ raise ArgumentError, "Unknown encryption strategy: #{encrypted_strategy}!"
end
end
@@ -41,8 +31,8 @@ module TokenAuthenticatableStrategies
return super if instance.has_attribute?(encrypted_field)
- if fully_encrypted?
- raise ArgumentError, 'Using encrypted strategy when encrypted field is missing!'
+ if required?
+ raise ArgumentError, 'Using required encryption strategy when encrypted field is missing!'
else
insecure_strategy.ensure_token(instance)
end
@@ -53,8 +43,7 @@ module TokenAuthenticatableStrategies
encrypted_token = instance.read_attribute(encrypted_field)
token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
-
- token || (insecure_strategy.get_token(instance) if fallback?)
+ token || (insecure_strategy.get_token(instance) if optional?)
end
def set_token(instance, token)
@@ -62,16 +51,35 @@ module TokenAuthenticatableStrategies
instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
instance[token_field] = token if migrating?
- instance[token_field] = nil if fallback?
+ instance[token_field] = nil if optional?
token
end
- def fully_encrypted?
- !migrating? && !fallback?
+ def required?
+ encrypted_strategy == :required
+ end
+
+ def migrating?
+ encrypted_strategy == :migrating
+ end
+
+ def optional?
+ encrypted_strategy == :optional
end
protected
+ def encrypted_strategy
+ value = options[:encrypted]
+ value = value.call if value.is_a?(Proc)
+
+ unless value.in?([:required, :optional, :migrating])
+ raise ArgumentError, 'encrypted: needs to be a :required, :optional or :migrating!'
+ end
+
+ value
+ end
+
def find_by_plaintext_token(token, unscoped)
insecure_strategy.find_token_authenticatable(token, unscoped)
end
@@ -89,7 +97,7 @@ module TokenAuthenticatableStrategies
def token_set?(instance)
raw_token = instance.read_attribute(encrypted_field)
- unless fully_encrypted?
+ unless required?
raw_token ||= insecure_strategy.get_token(instance)
end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index d79c0eae77e..6c6febd186c 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -27,40 +27,14 @@ module WithUploads
included do
has_many :uploads, as: :model
- has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) }, class_name: 'Upload', as: :model
+ has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) },
+ class_name: 'Upload', as: :model,
+ dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- # TODO: when feature flag is removed, we can use just dependent: destroy
- # option on :file_uploads
- before_destroy :remove_file_uploads
-
- use_fast_destroy :file_uploads, if: :fast_destroy_enabled?
+ use_fast_destroy :file_uploads
end
def retrieve_upload(_identifier, paths)
uploads.find_by(path: paths)
end
-
- private
-
- # mounted uploads are deleted in carrierwave's after_commit hook,
- # but FileUploaders which are not mounted must be deleted explicitly and
- # it can not be done in after_commit because FileUploader requires loads
- # associated model on destroy (which is already deleted in after_commit)
- def remove_file_uploads
- fast_destroy_enabled? ? delete_uploads : destroy_uploads
- end
-
- def delete_uploads
- file_uploads.delete_all(:delete_all)
- end
-
- def destroy_uploads
- file_uploads.find_each do |upload|
- upload.destroy
- end
- end
-
- def fast_destroy_enabled?
- Feature.enabled?(:fast_destroy_uploads, self)
- end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 2c08a8e1acf..cf057d774cf 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ContainerRepository < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
+
belongs_to :project
validates :name, length: { minimum: 0, allow_nil: false }
@@ -8,6 +10,8 @@ class ContainerRepository < ActiveRecord::Base
delegate :client, to: :registry
+ scope :ordered, -> { order(:name) }
+
# rubocop: disable CodeReuse/ServiceClass
def registry
@registry ||= begin
@@ -39,11 +43,12 @@ class ContainerRepository < ActiveRecord::Base
end
def tags
- return @tags if defined?(@tags)
return [] unless manifest && manifest['tags']
- @tags = manifest['tags'].map do |tag|
- ContainerRegistry::Tag.new(self, tag)
+ strong_memoize(:tags) do
+ manifest['tags'].sort.map do |tag|
+ ContainerRegistry::Tag.new(self, tag)
+ end
end
end
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index ad0bb55f0a7..74aa04ab7d0 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -6,34 +6,17 @@ class DashboardGroupMilestone < GlobalMilestone
attr_reader :group_name
def initialize(milestone)
- super(milestone.title, Array(milestone))
+ super
@group_name = milestone.group.full_name
end
- def self.build_collection(groups)
- Milestone.of_groups(groups.select(:id))
+ def self.build_collection(groups, params)
+ milestones = Milestone.of_groups(groups.select(:id))
.reorder_by_due_date_asc
.order_by_name_asc
.active
- .map { |m| new(m) }
- end
-
- override :group_milestone?
- def group_milestone?
- @first_milestone.group_milestone?
- end
-
- override :milestoneish_ids
- def milestoneish_ids
- milestones.map(&:id)
- end
-
- def group
- @first_milestone.group
- end
-
- def iid
- @first_milestone.iid
+ milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
+ milestones.map { |m| new(m) }
end
end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
index 96bc8090b81..9b377b70e5b 100644
--- a/app/models/dashboard_milestone.rb
+++ b/app/models/dashboard_milestone.rb
@@ -1,11 +1,15 @@
# frozen_string_literal: true
class DashboardMilestone < GlobalMilestone
- def issues_finder_params
- { authorized_only: true }
+ attr_reader :project_name
+
+ def initialize(milestone)
+ super
+
+ @project_name = milestone.project.full_name
end
- def dashboard_milestone?
+ def project_milestone?
true
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 279603496b0..805092e527a 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -41,6 +41,14 @@ class DiffNote < Note
create_note_diff_file(creation_params)
end
+ # Returns the diff file from `position`
+ def latest_diff_file
+ strong_memoize(:latest_diff_file) do
+ position.diff_file(project.repository)
+ end
+ end
+
+ # Returns the diff file from `original_position`
def diff_file
strong_memoize(:diff_file) do
enqueue_diff_file_creation_job if should_create_diff_file?
diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb
index 1176861a827..527ee33b83b 100644
--- a/app/models/diff_viewer/base.rb
+++ b/app/models/diff_viewer/base.rb
@@ -18,7 +18,7 @@ module DiffViewer
def initialize(diff_file)
@diff_file = diff_file
- @initially_binary = diff_file.binary?
+ @initially_binary = diff_file.binary_in_repo?
end
def self.partial_path
@@ -48,7 +48,7 @@ module DiffViewer
def self.can_render_blob?(blob, verify_binary: true)
return true if blob.nil?
- return false if verify_binary && binary? != blob.binary?
+ return false if verify_binary && binary? != blob.binary_in_repo?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(blob.file_type)
@@ -70,20 +70,49 @@ module DiffViewer
end
def binary_detected_after_load?
- !@initially_binary && diff_file.binary?
+ !@initially_binary && diff_file.binary_in_repo?
end
# This method is used on the server side to check whether we can attempt to
- # render the diff_file at all. Human-readable error messages are found in the
- # `BlobHelper#diff_render_error_reason` helper.
+ # render the diff_file at all. The human-readable error message can be
+ # retrieved by #render_error_message.
def render_error
if too_large?
:too_large
end
end
+ def render_error_message
+ return unless render_error
+
+ _("This %{viewer} could not be displayed because %{reason}. You can %{options} instead.") %
+ {
+ viewer: switcher_title,
+ reason: render_error_reason,
+ options: render_error_options.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
+ }
+ end
+
def prepare!
# To be overridden by subclasses
end
+
+ private
+
+ def render_error_options
+ options = []
+
+ blob_url = Gitlab::Routing.url_helpers.project_blob_path(diff_file.repository.project,
+ File.join(diff_file.content_sha, diff_file.file_path))
+ options << ActionController::Base.helpers.link_to(_('view the blob'), blob_url)
+
+ options
+ end
+
+ def render_error_reason
+ if render_error == :too_large
+ _("it is too large")
+ end
+ end
end
end
diff --git a/app/models/diff_viewer/image.rb b/app/models/diff_viewer/image.rb
index c356c2ca50e..350bef1d42a 100644
--- a/app/models/diff_viewer/image.rb
+++ b/app/models/diff_viewer/image.rb
@@ -9,6 +9,6 @@ module DiffViewer
self.extensions = UploaderHelper::IMAGE_EXT
self.binary = true
self.switcher_icon = 'picture-o'
- self.switcher_title = 'image diff'
+ self.switcher_title = _('image diff')
end
end
diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb
index 2faa1be6567..5caefa2031c 100644
--- a/app/models/diff_viewer/rich.rb
+++ b/app/models/diff_viewer/rich.rb
@@ -7,7 +7,7 @@ module DiffViewer
included do
self.type = :rich
self.switcher_icon = 'file-text-o'
- self.switcher_title = 'rendered diff'
+ self.switcher_title = _('rendered diff')
end
end
end
diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb
index 977204e6c97..0877c9dddec 100644
--- a/app/models/diff_viewer/server_side.rb
+++ b/app/models/diff_viewer/server_side.rb
@@ -24,5 +24,17 @@ module DiffViewer
super
end
+
+ private
+
+ def render_error_reason
+ return super unless render_error == :server_side_but_stored_externally
+
+ if diff_file.external_storage == :lfs
+ _('it is stored in LFS')
+ else
+ _('it is stored externally')
+ end
+ end
end
end
diff --git a/app/models/diff_viewer/simple.rb b/app/models/diff_viewer/simple.rb
index 8d28ca5239a..929d8ad5a7e 100644
--- a/app/models/diff_viewer/simple.rb
+++ b/app/models/diff_viewer/simple.rb
@@ -7,7 +7,7 @@ module DiffViewer
included do
self.type = :simple
self.switcher_icon = 'code'
- self.switcher_title = 'source diff'
+ self.switcher_title = _('source diff')
end
end
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index dbc7b6e67be..32529ebf71d 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -116,6 +116,10 @@ class Discussion
false
end
+ def can_convert_to_discussion?
+ false
+ end
+
def new_discussion?
notes.length == 1
end
diff --git a/app/models/email.rb b/app/models/email.rb
index b6a977dfa22..3ce6e792fa8 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -15,7 +15,7 @@ class Email < ActiveRecord::Base
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
devise :confirmable
- self.reconfirmable = false # currently email can't be changed, no need to reconfirm
+ self.reconfirmable = false # currently email can't be changed, no need to reconfirm
delegate :username, to: :user
diff --git a/app/models/environment.rb b/app/models/environment.rb
index cdfe3b7c023..3d909cc8e5c 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -50,6 +50,14 @@ class Environment < ActiveRecord::Base
end
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
+
+ ##
+ # Search environments which have names like the given query.
+ # Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
+ scope :for_name_like, -> (query, limit: 5) do
+ where('name LIKE ?', "#{sanitize_sql_like(query)}%").limit(limit)
+ end
+
scope :for_project, -> (project) { where(project_id: project) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
@@ -70,6 +78,10 @@ class Environment < ActiveRecord::Base
end
end
+ def self.pluck_names
+ pluck(:name)
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
@@ -107,7 +119,7 @@ class Environment < ActiveRecord::Base
def first_deployment_for(commit_sha)
ref = project.repository.ref_name_for_sha(ref_path, commit_sha)
- return nil unless ref
+ return unless ref
deployment_iid = ref.split('/').last
deployments.find_by(iid: deployment_iid)
@@ -118,7 +130,7 @@ class Environment < ActiveRecord::Base
end
def formatted_external_url
- return nil unless external_url
+ return unless external_url
external_url.gsub(%r{\A.*?://}, '')
end
@@ -231,6 +243,10 @@ class Environment < ActiveRecord::Base
self.environment_type || self.name
end
+ def name_without_type
+ @name_without_type ||= name.delete_prefix("#{environment_type}/")
+ end
+
def deployment_platform
strong_memoize(:deployment_platform) do
project.deployment_platform(environment: self.name)
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
new file mode 100644
index 00000000000..1e2bd3bda7f
--- /dev/null
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectErrorTrackingSetting < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
+ include ReactiveCaching
+
+ API_URL_PATH_REGEXP = %r{
+ \A
+ (?<prefix>/api/0/projects/+)
+ (?:
+ (?<organization>[^/]+)/+
+ (?<project>[^/]+)/*
+ )?
+ \z
+ }x
+
+ self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
+
+ belongs_to :project
+
+ validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
+
+ validates :api_url, presence: { message: 'is a required field' }, if: :enabled
+
+ validate :validate_api_url_path, if: :enabled
+
+ validates :token, presence: { message: 'is a required field' }, if: :enabled
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm'
+
+ after_save :clear_reactive_cache!
+
+ def api_url=(value)
+ super
+ clear_memoization(:api_url_slugs)
+ end
+
+ def project_name
+ super || project_name_from_slug
+ end
+
+ def organization_name
+ super || organization_name_from_slug
+ end
+
+ def project_slug
+ project_slug_from_api_url
+ end
+
+ def organization_slug
+ organization_slug_from_api_url
+ end
+
+ def self.build_api_url_from(api_host:, project_slug:, organization_slug:)
+ return if api_host.blank?
+
+ uri = Addressable::URI.parse("#{api_host}/api/0/projects/#{organization_slug}/#{project_slug}/")
+ uri.path = uri.path.squeeze('/')
+
+ uri.to_s
+ rescue Addressable::URI::InvalidURIError
+ api_host
+ end
+
+ def sentry_client
+ Sentry::Client.new(api_url, token)
+ end
+
+ def sentry_external_url
+ self.class.extract_sentry_external_url(api_url)
+ end
+
+ def list_sentry_issues(opts = {})
+ with_reactive_cache('list_issues', opts.stringify_keys) do |result|
+ result
+ end
+ end
+
+ def list_sentry_projects
+ { projects: sentry_client.list_projects }
+ end
+
+ def calculate_reactive_cache(request, opts)
+ case request
+ when 'list_issues'
+ { issues: sentry_client.list_issues(**opts.symbolize_keys) }
+ end
+ rescue Sentry::Client::Error => e
+ { error: e.message }
+ end
+
+ # http://HOST/api/0/projects/ORG/PROJECT
+ # ->
+ # http://HOST/ORG/PROJECT
+ def self.extract_sentry_external_url(url)
+ url.sub('api/0/projects/', '')
+ end
+
+ def api_host
+ return if api_url.blank?
+
+ # This returns http://example.com/
+ Addressable::URI.join(api_url, '/').to_s
+ end
+
+ private
+
+ def project_name_from_slug
+ @project_name_from_slug ||= project_slug_from_api_url&.titleize
+ end
+
+ def organization_name_from_slug
+ @organization_name_from_slug ||= organization_slug_from_api_url&.titleize
+ end
+
+ def project_slug_from_api_url
+ api_url_slug(:project)
+ end
+
+ def organization_slug_from_api_url
+ api_url_slug(:organization)
+ end
+
+ def api_url_slug(capture)
+ slugs = strong_memoize(:api_url_slugs) { extract_api_url_slugs || {} }
+ slugs[capture]
+ end
+
+ def extract_api_url_slugs
+ return if api_url.blank?
+
+ begin
+ url = Addressable::URI.parse(api_url)
+ rescue Addressable::URI::InvalidURIError
+ return
+ end
+
+ url.path.match(API_URL_PATH_REGEXP)
+ end
+
+ def validate_api_url_path
+ return if api_url.blank?
+
+ unless api_url_slug(:prefix)
+ return errors.add(:api_url, 'is invalid')
+ end
+
+ unless api_url_slug(:organization)
+ errors.add(:project, 'is a required field')
+ end
+ end
+ end
+end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 4f73beaafc5..68b2353556e 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -3,6 +3,8 @@
class ExternalIssue
include Referable
+ attr_reader :project
+
def initialize(issue_identifier, project)
@issue_identifier, @project = issue_identifier, project
end
@@ -32,12 +34,8 @@ class ExternalIssue
[self.class, to_s].hash
end
- def project
- @project
- end
-
def project_id
- @project.id
+ project.id
end
def to_reference(_from = nil, full: nil)
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 085ffd16c6a..fd17745b035 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -3,69 +3,79 @@
class GlobalMilestone
include Milestoneish
- EPOCH = DateTime.parse('1970-01-01')
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
- attr_accessor :title, :milestones
+ attr_reader :milestone
alias_attribute :name, :title
+ delegate :title, :state, :due_date, :start_date, :participants, :project, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, :milestoneish_id, to: :milestone
+
+ def to_hash
+ {
+ name: title,
+ title: title,
+ group_name: group&.full_name,
+ project_name: project&.full_name
+ }
+ end
+
def for_display
- @first_milestone
+ @milestone
end
def self.build_collection(projects, params)
- params =
- { project_ids: projects.map(&:id), state: params[:state] }
-
- child_milestones = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
-
- milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
- milestones_relation = Milestone.where(id: grouped.map(&:id))
- new(title, milestones_relation)
- end
+ items = Milestone.of_projects(projects)
+ .reorder_by_due_date_asc
+ .order_by_name_asc
+ items = items.search_title(params[:search_title]) if params[:search_title].present?
- milestones.sort_by { |milestone| milestone.due_date || EPOCH }
+ Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
end
+ # necessary for legacy milestones
def self.build(projects, title)
- child_milestones = Milestone.of_projects(projects).where(title: title)
- return if child_milestones.blank?
+ milestones = Milestone.of_projects(projects).where(title: title)
+ return if milestones.blank?
- new(title, child_milestones)
+ new(milestones.first)
end
- def self.count_by_state(milestones_by_state_and_title, state)
- milestones_by_state_and_title.count do |(milestone_state, _), _|
- milestone_state == state
+ def self.states_count(projects, group = nil)
+ legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
+ group_milestones_count = group_milestones_states_count(group)
+
+ legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
+ legacy_group_milestones_count + group_milestones_count
end
end
- private_class_method :count_by_state
- def initialize(title, milestones)
- @title = title
- @name = title
- @milestones = milestones
- @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
- end
+ def self.group_milestones_states_count(group)
+ return STATE_COUNT_HASH unless group
- def milestoneish_ids
- milestones.select(:id)
- end
+ counts_by_state = Milestone.of_groups(group).count_by_state
- def safe_title
- @title.to_slug.normalize.to_s
+ {
+ opened: counts_by_state['active'] || 0,
+ closed: counts_by_state['closed'] || 0,
+ all: counts_by_state.values.sum
+ }
end
- def projects
- @projects ||= Project.for_milestones(milestoneish_ids)
- end
+ def self.legacy_group_milestone_states_count(projects)
+ return STATE_COUNT_HASH unless projects
- def state
- milestones.each do |milestone|
- return 'active' if milestone.state != 'closed'
- end
+ # We need to reorder(nil) on the projects, because the controller passes them in sorted.
+ relation = Milestone.of_projects(projects.reorder(nil)).count_by_state
- 'closed'
+ {
+ opened: relation['active'] || 0,
+ closed: relation['closed'] || 0,
+ all: relation.values.sum
+ }
+ end
+
+ def initialize(milestone)
+ @milestone = milestone
end
def active?
@@ -77,37 +87,14 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels)
+ @issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
- end
-
- def participants
- @participants ||= milestones.map(&:participants).flatten.uniq
+ @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignee, :labels)
end
def labels
- @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
- .sort_by!(&:title)
- end
-
- def due_date
- return @due_date if defined?(@due_date)
-
- @due_date =
- if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
- @milestones.first.due_date
- end
- end
-
- def start_date
- return @start_date if defined?(@start_date)
-
- @start_date =
- if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
- @milestones.first.start_date
- end
+ @labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
end
end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 0816778deae..7f9ff7bbda6 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class GpgSignature < ActiveRecord::Base
+class GpgSignature < ApplicationRecord
include ShaAttribute
sha_attribute :commit_sha
@@ -33,6 +33,11 @@ class GpgSignature < ActiveRecord::Base
)
end
+ def self.safe_create!(attributes)
+ create_with(attributes)
+ .safe_find_or_create_by!(commit_sha: attributes[:commit_sha])
+ end
+
def gpg_key=(model)
case model
when GpgKey
diff --git a/app/models/group.rb b/app/models/group.rb
index edac2444c4d..495bfe04499 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -56,7 +56,7 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
- add_authentication_token_field :runners_token, encrypted: true, migrating: true
+ add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption) ? :optional : :required }
after_create :post_create_hook
after_destroy :post_destroy_hook
@@ -98,7 +98,7 @@ class Group < Namespace
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
- .where('project_namespace.share_with_group_lock = ?', false)
+ .where('project_namespace.share_with_group_lock = ?', false)
.select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
@@ -382,6 +382,10 @@ class Group < Namespace
end
end
+ def highest_group_member(user)
+ GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
+ end
+
def hashed_storage?(_feature)
false
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 9dfaebacc83..97cb26c6ea9 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -1,18 +1,36 @@
# frozen_string_literal: true
# Group Milestones are milestones that can be shared among many projects within the same group
class GroupMilestone < GlobalMilestone
- attr_accessor :group
+ attr_reader :group, :milestones
def self.build_collection(group, projects, params)
- super(projects, params).each do |milestone|
- milestone.group = group
+ params =
+ { state: params[:state], search_title: params[:search_title] }
+
+ project_milestones = Milestone.of_projects(projects)
+ project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present?
+ child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
+ grouped_milestones = child_milestones.group_by(&:title)
+
+ grouped_milestones.map do |title, grouped|
+ new(title, grouped, group)
end
end
def self.build(group, projects, title)
- super(projects, title).tap do |milestone|
- milestone&.group = group
- end
+ child_milestones = Milestone.of_projects(projects).where(title: title)
+ return if child_milestones.blank?
+
+ new(title, child_milestones, group)
+ end
+
+ def initialize(title, milestones, group)
+ @milestones = milestones
+ @group = group
+ end
+
+ def milestone
+ @milestone ||= milestones.find { |m| m.description.present? } || milestones.first
end
def issues_finder_params
diff --git a/app/models/identity.rb b/app/models/identity.rb
index d63dd432426..acdde4f296b 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,7 +8,7 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: UniquenessScopes.scopes, case_sensitive: false }
- validates :user_id, uniqueness: { scope: UniquenessScopes.scopes }
+ validates :user, uniqueness: { scope: UniquenessScopes.scopes }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
index 07ee7470ea2..b4a661ae5b4 100644
--- a/app/models/individual_note_discussion.rb
+++ b/app/models/individual_note_discussion.rb
@@ -13,6 +13,18 @@ class IndividualNoteDiscussion < Discussion
true
end
+ def can_convert_to_discussion?
+ noteable.supports_replying_to_individual_notes? && Feature.enabled?(:reply_to_individual_notes)
+ end
+
+ def convert_to_discussion!(save: false)
+ first_note.becomes!(Discussion.note_class).to_discussion.tap do
+ # Save needs to be called on first_note instead of the transformed note
+ # because of https://gitlab.com/gitlab-org/gitlab-ce/issues/57324
+ first_note.save if save
+ end
+ end
+
def reply_attributes
super.tap { |attrs| attrs.delete(:discussion_id) }
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index e7168d49db9..e75c6eb2331 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -66,6 +66,17 @@ class InternalId < ActiveRecord::Base
InternalIdGenerator.new(subject, scope, usage, init).generate
end
+ # Flushing records is generally safe in a sense that those
+ # records are going to be re-created when needed.
+ #
+ # A filter condition has to be provided to not accidentally flush
+ # records for all projects.
+ def flush_records!(filter)
+ raise ArgumentError, "filter cannot be empty" if filter.blank?
+
+ where(filter).delete_all
+ end
+
def available?
@available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
end
@@ -111,7 +122,7 @@ class InternalId < ActiveRecord::Base
# Generates next internal id and returns it
def generate
- InternalId.transaction do
+ subject.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
#
@@ -125,7 +136,7 @@ class InternalId < ActiveRecord::Base
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
- InternalId.transaction do
+ subject.transaction do
(lookup || create_record).track_greatest_and_save!(new_value)
end
end
@@ -148,7 +159,7 @@ class InternalId < ActiveRecord::Base
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
def create_record
- InternalId.transaction(requires_new: true) do
+ subject.transaction(requires_new: true) do
InternalId.create!(
**scope,
usage: usage_value,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b7e13bcbccf..071ad50fddc 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -26,6 +26,8 @@ class Issue < ActiveRecord::Base
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
+ SORTING_PREFERENCE_FIELD = :issues_sort
+
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
@@ -64,6 +66,7 @@ class Issue < ActiveRecord::Base
scope :preload_associations, -> { preload(:labels, project: :namespace) }
scope :public_only, -> { where(confidential: false) }
+ scope :confidential_only, -> { where(confidential: true) }
after_save :expire_etag_cache
after_save :ensure_metrics, unless: :imported?
@@ -230,7 +233,8 @@ class Issue < ActiveRecord::Base
end
def check_for_spam?
- project.public? && (title_changed? || description_changed?)
+ publicly_visible? &&
+ (title_changed? || description_changed? || confidential_changed?)
end
def as_json(options = {})
@@ -259,6 +263,10 @@ class Issue < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
+ def merge_requests_count
+ merge_requests_closing_issues.count
+ end
+
private
def ensure_metrics
diff --git a/app/models/label.rb b/app/models/label.rb
index 5d2d1afd1d9..1c3db3eb35d 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -214,6 +214,7 @@ class Label < ActiveRecord::Base
super(options).tap do |json|
json[:type] = self.try(:type)
json[:priority] = priority(options[:project]) if options.key?(:project)
+ json[:textColor] = text_color
end
end
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
index 680952cf421..d6814f4a948 100644
--- a/app/models/label_note.rb
+++ b/app/models/label_note.rb
@@ -81,7 +81,7 @@ class LabelNote < Note
deleted = label_refs.count - existing_refs.count
deleted_str = deleted == 0 ? nil : "#{deleted} deleted"
- return nil unless refs_str || deleted_str
+ return unless refs_str || deleted_str
label_list_str = [refs_str, deleted_str].compact.join(' + ')
suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count)
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 00dec6bb92b..e2c75bc7ee9 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -73,7 +73,7 @@ class LegacyDiffNote < Note
private
def find_diff
- return nil unless noteable
+ return unless noteable
return @diff if defined?(@diff)
@diff = noteable.raw_diffs(Commit.max_diff_options).find do |d|
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
new file mode 100644
index 00000000000..6383f95d546
--- /dev/null
+++ b/app/models/lfs_download_object.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class LfsDownloadObject
+ include ActiveModel::Validations
+
+ attr_accessor :oid, :size, :link
+ delegate :sanitized_url, :credentials, to: :sanitized_uri
+
+ validates :oid, format: { with: /\A\h{64}\z/ }
+ validates :size, numericality: { greater_than_or_equal_to: 0 }
+ validates :link, public_url: { protocols: %w(http https) }
+
+ def initialize(oid:, size:, link:)
+ @oid = oid
+ @size = size
+ @link = link
+ end
+
+ def sanitized_uri
+ @sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
+ end
+end
diff --git a/app/models/list.rb b/app/models/list.rb
index 029685be927..682af761ba0 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -54,6 +54,6 @@ class List < ActiveRecord::Base
private
def can_be_destroyed
- destroyable?
+ throw(:abort) unless destroyable?
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 9fc95ea00c3..8e071a8ff21 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -76,14 +76,17 @@ class Member < ActiveRecord::Base
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
scope :masters, -> { maintainers } # @deprecated
scope :owners, -> { active.where(access_level: OWNER) }
- scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
+ scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
+ scope :with_user, -> (user) { where(user: user) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+ scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index fc49ee7ac8c..2c9e1ba1d80 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,6 +12,8 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
+ scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
+
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 016c18ce6c8..5372c6084f4 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -12,6 +12,10 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
+ scope :in_namespaces, ->(groups) do
+ joins('INNER JOIN projects ON projects.id = members.source_id')
+ .where('projects.namespace_id in (?)', groups.select(:id))
+ end
class << self
# Add users to projects with passed access option
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 944b9f72396..b67797f5404 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -21,6 +21,8 @@ class MergeRequest < ActiveRecord::Base
self.reactive_cache_refresh_interval = 10.minutes
self.reactive_cache_lifetime = 10.minutes
+ SORTING_PREFERENCE_FIELD = :merge_requests_sort
+
ignore_column :locked_at,
:ref_fetched,
:deleted_at
@@ -69,7 +71,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
- after_create :ensure_merge_request_diff, unless: :importing?
+ after_create :ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
after_save :ensure_metrics
@@ -105,7 +107,9 @@ class MergeRequest < ActiveRecord::Base
before_transition any => :opened do |merge_request|
merge_request.merge_jid = nil
+ end
+ after_transition any => :opened do |merge_request|
merge_request.run_after_commit do
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
@@ -185,6 +189,9 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit
+ alias_attribute :project, :target_project
+ alias_attribute :project_id, :target_project_id
+
def self.reference_prefix
'!'
end
@@ -282,6 +289,14 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
+ def commit_authors
+ @commit_authors ||= commits.authors
+ end
+
+ def authors
+ User.from_union([commit_authors, User.where(id: self.author_id)])
+ end
+
# Verifies if title has changed not taking into account WIP prefix
# for merge requests.
def wipless_title_changed(old_title)
@@ -325,13 +340,15 @@ class MergeRequest < ActiveRecord::Base
end
def commits
- if persisted?
- merge_request_diff.commits
- elsif compare_commits
- compare_commits.reverse
- else
- []
- end
+ return merge_request_diff.commits if persisted?
+
+ commits_arr = if compare_commits
+ compare_commits.reverse
+ else
+ []
+ end
+
+ CommitCollection.new(source_project, commits_arr, source_branch)
end
def commits_count
@@ -408,6 +425,28 @@ class MergeRequest < ActiveRecord::Base
merge_request_diffs.where.not(id: merge_request_diff.id)
end
+ def preloads_discussion_diff_highlighting?
+ true
+ end
+
+ def preload_discussions_diff_highlight
+ preloadable_files = note_diff_files.for_commit_or_unresolved
+
+ discussions_diffs.load_highlight(preloadable_files.pluck(:id))
+ end
+
+ def discussions_diffs
+ strong_memoize(:discussions_diffs) do
+ Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
+ end
+ end
+
+ def note_diff_files
+ NoteDiffFile
+ .where(diff_note: discussion_notes)
+ .includes(diff_note: :project)
+ end
+
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
@@ -526,15 +565,19 @@ class MergeRequest < ActiveRecord::Base
end
def diff_refs
- if persisted?
- merge_request_diff.diff_refs
- else
- Gitlab::Diff::DiffRefs.new(
- base_sha: diff_base_sha,
- start_sha: diff_start_sha,
- head_sha: diff_head_sha
- )
- end
+ persisted? ? merge_request_diff.diff_refs : repository_diff_refs
+ end
+
+ # Instead trying to fetch the
+ # persisted diff_refs, this method goes
+ # straight to the repository to get the
+ # most recent data possible.
+ def repository_diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: branch_merge_base_sha,
+ start_sha: target_branch_sha,
+ head_sha: source_branch_sha
+ )
end
def branch_merge_base_sha
@@ -724,6 +767,16 @@ class MergeRequest < ActiveRecord::Base
true
end
+ def mergeable_to_ref?
+ return false if merged?
+ return false if broken?
+
+ # Given the `merge_ref_path` will have the same
+ # state the `target_branch` would have. Ideally
+ # we need to check if it can be merged to it.
+ project.repository.can_be_merged?(diff_head_sha, target_branch)
+ end
+
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -797,10 +850,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
@@ -899,7 +948,7 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_exists?(self.target_branch)
end
- def merge_commit_message(include_description: false)
+ def default_merge_commit_message(include_description: false)
closes_issues_references = visible_closing_issues_for.map do |issue|
issue.to_reference(target_project)
end
@@ -919,6 +968,13 @@ class MergeRequest < ActiveRecord::Base
message.join("\n\n")
end
+ # Returns the oldest multi-line commit message, or the MR title if none found
+ def default_squash_commit_message
+ strong_memoize(:default_squash_commit_message) do
+ commits.without_merge_commits.reverse.find(&:description?)&.safe_message || title
+ end
+ end
+
def reset_merge_when_pipeline_succeeds
return unless merge_when_pipeline_succeeds?
@@ -927,6 +983,7 @@ class MergeRequest < ActiveRecord::Base
if merge_params
merge_params.delete('should_remove_source_branch')
merge_params.delete('commit_message')
+ merge_params.delete('squash_commit_message')
end
self.save
@@ -1029,6 +1086,10 @@ class MergeRequest < ActiveRecord::Base
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
+ def merge_ref_path
+ "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
+ end
+
def in_locked_state
begin
lock_mr
@@ -1068,10 +1129,16 @@ class MergeRequest < ActiveRecord::Base
def all_pipelines(shas: all_commit_shas)
return Ci::Pipeline.none unless source_project
- @all_pipelines ||= source_project.ci_pipelines
- .where(sha: shas, ref: source_branch)
- .where(merge_request: [nil, self])
- .sort_by_merge_request_pipelines
+ @all_pipelines ||=
+ source_project.ci_pipelines
+ .for_merge_request(self, source_branch, all_commit_shas)
+ end
+
+ def update_head_pipeline
+ find_actual_head_pipeline.try do |pipeline|
+ self.head_pipeline = pipeline
+ update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed?
+ end
end
def merge_request_pipeline_exists?
@@ -1086,35 +1153,16 @@ class MergeRequest < ActiveRecord::Base
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
-
- variables.append(key: 'CI_MERGE_REQUEST_REF_PATH',
- value: ref_path.to_s)
-
- variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID',
- value: project.id.to_s)
-
- variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH',
- value: project.full_path)
-
- variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL',
- value: project.web_url)
-
- variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME',
- value: target_branch.to_s)
-
- if source_project
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID',
- value: source_project.id.to_s)
-
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH',
- value: source_project.full_path)
-
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL',
- value: source_project.web_url)
-
- variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME',
- value: source_branch.to_s)
- end
+ variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path)
+ variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
+ variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
+ variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
+ variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
+ variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
+ variables.concat(source_project_variables)
end
end
@@ -1268,7 +1316,7 @@ class MergeRequest < ActiveRecord::Base
def base_pipeline
@base_pipeline ||= project.ci_pipelines
.order(id: :desc)
- .find_by(sha: diff_base_sha)
+ .find_by(sha: diff_base_sha, ref: target_branch)
end
def discussions_rendered_on_frontend?
@@ -1314,4 +1362,22 @@ class MergeRequest < ActiveRecord::Base
source_project.repository.squash_in_progress?(id)
end
+
+ private
+
+ def find_actual_head_pipeline
+ source_project&.ci_pipelines
+ &.latest_for_merge_request(self, source_branch, diff_head_sha)
+ end
+
+ def source_project_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless source_project
+
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', value: source_project.id.to_s)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url)
+ variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s)
+ end
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index a3029a54604..351a662ae83 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -7,19 +7,23 @@ class MergeRequestDiff < ActiveRecord::Base
include IgnorableColumn
include EachBatch
include Gitlab::Utils::StrongMemoize
+ include ObjectStorage::BackgroundMove
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
- ignore_column :st_commits,
- :st_diffs
-
belongs_to :merge_request
+
manual_inverse_association :merge_request, :merge_request_diff
- has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
+ has_many :merge_request_diff_files,
+ -> { order(:merge_request_diff_id, :relative_order) },
+ inverse_of: :merge_request_diff
+
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
+ validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true
+
state_machine :state, initial: :empty do
event :clean do
transition any => :without_files
@@ -45,10 +49,14 @@ class MergeRequestDiff < ActiveRecord::Base
scope :recent, -> { order(id: :desc).limit(100) }
+ mount_uploader :external_diff, ExternalDiffUploader
+
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
+ after_save :update_external_diff_store, if: :external_diff_changed?
+
def self.find_by_diff_refs(diff_refs)
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
@@ -241,10 +249,97 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
+ # Carrierwave defines `write_uploader` dynamically on this class, so `super`
+ # does not work. Alias the carrierwave method so we can call it when needed
+ alias_method :carrierwave_write_uploader, :write_uploader
+
+ # The `external_diff`, `external_diff_store`, and `stored_externally`
+ # columns were introduced in GitLab 11.8, but some background migration specs
+ # use factories that rely on current code with an old schema. Without these
+ # `has_attribute?` guards, they fail with a `MissingAttributeError`.
+ #
+ # For more details, see: https://gitlab.com/gitlab-org/gitlab-ce/issues/44990
+
+ def write_uploader(column, identifier)
+ carrierwave_write_uploader(column, identifier) if has_attribute?(column)
+ end
+
+ def update_external_diff_store
+ update_column(:external_diff_store, external_diff.object_store) if
+ has_attribute?(:external_diff_store)
+ end
+
+ def external_diff_changed?
+ super if has_attribute?(:external_diff)
+ end
+
+ def stored_externally
+ super if has_attribute?(:stored_externally)
+ end
+ alias_method :stored_externally?, :stored_externally
+
+ # If enabled, yields the external file containing the diff. Otherwise, yields
+ # nil. This method is not thread-safe, but it *is* re-entrant, which allows
+ # multiple merge_request_diff_files to load their data efficiently
+ def opening_external_diff
+ return yield(nil) unless stored_externally?
+ return yield(@external_diff_file) if @external_diff_file
+
+ external_diff.open do |file|
+ begin
+ @external_diff_file = file
+
+ yield(@external_diff_file)
+ ensure
+ @external_diff_file = nil
+ end
+ end
+ end
+
private
def create_merge_request_diff_files(diffs)
- rows = diffs.map.with_index do |diff, index|
+ rows =
+ if has_attribute?(:external_diff) && Gitlab.config.external_diffs.enabled
+ build_external_merge_request_diff_files(diffs)
+ else
+ build_merge_request_diff_files(diffs)
+ end
+
+ # Faster inserts
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ end
+
+ def build_external_merge_request_diff_files(diffs)
+ rows = build_merge_request_diff_files(diffs)
+ tempfile = build_external_diff_tempfile(rows)
+
+ self.external_diff = tempfile
+ self.stored_externally = true
+
+ rows
+ ensure
+ tempfile&.unlink
+ end
+
+ def build_external_diff_tempfile(rows)
+ Tempfile.open(external_diff.filename) do |file|
+ rows.inject(0) do |offset, row|
+ data = row.delete(:diff)
+ row[:external_diff_offset] = offset
+ row[:external_diff_size] = data.size
+
+ file.write(data)
+
+ offset + data.size
+ end
+
+ file
+ end
+ end
+
+ def build_merge_request_diff_files(diffs)
+ diffs.map.with_index do |diff, index|
diff_hash = diff.to_hash.merge(
binary: false,
merge_request_diff_id: self.id,
@@ -261,18 +356,20 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
end
-
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
end
def load_diffs(options)
- collection = merge_request_diff_files
+ # Ensure all diff files operate on the same external diff file instance if
+ # present. This reduces file open/close overhead.
+ opening_external_diff do
+ collection = merge_request_diff_files
- if paths = options[:paths]
- collection = collection.where('old_path IN (?) OR new_path IN (?)', paths, paths)
- end
+ if paths = options[:paths]
+ collection = collection.where('old_path IN (?) OR new_path IN (?)', paths, paths)
+ end
- Gitlab::Git::DiffCollection.new(collection.map(&:to_hash), options)
+ Gitlab::Git::DiffCollection.new(collection.map(&:to_hash), options)
+ end
end
def load_commits
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index a9f110bec5c..e8d936e265c 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -4,7 +4,7 @@ class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper
include DiffFile
- belongs_to :merge_request_diff
+ belongs_to :merge_request_diff, inverse_of: :merge_request_diff_files
def utf8_diff
return '' if diff.blank?
@@ -13,6 +13,16 @@ class MergeRequestDiffFile < ActiveRecord::Base
end
def diff
- binary? ? super.unpack('m0').first : super
+ content =
+ if merge_request_diff&.stored_externally?
+ merge_request_diff.opening_external_diff do |file|
+ file.seek(external_diff_offset)
+ file.read(external_diff_size)
+ end
+ else
+ super
+ end
+
+ binary? ? content.unpack('m0').first : content
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 3cc8e2c44bb..26cfdc5ef30 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -28,7 +28,7 @@ class Milestone < ActiveRecord::Base
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -38,12 +38,14 @@ class Milestone < ActiveRecord::Base
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
- scope :for_projects_and_groups, -> (project_ids, group_ids) do
- conditions = []
- conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any?
- conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any?
+ scope :for_projects_and_groups, -> (projects, groups) do
+ projects = projects.compact if projects.is_a? Array
+ projects = [] if projects.nil?
- where(conditions.reduce(:or))
+ groups = groups.compact if groups.is_a? Array
+ groups = [] if groups.nil?
+
+ where(project_id: projects).or(where(group_id: groups))
end
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
@@ -75,7 +77,7 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
- # Searches for milestones matching the given query.
+ # Searches for milestones with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
@@ -86,6 +88,17 @@ class Milestone < ActiveRecord::Base
fuzzy_search(query, [:title, :description])
end
+ # Searches for milestones with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search_title(query)
+ fuzzy_search(query, [:title])
+ end
+
def filter_by_state(milestones, state)
case state
when 'closed' then milestones.closed
@@ -94,6 +107,10 @@ class Milestone < ActiveRecord::Base
end
end
+ def count_by_state
+ reorder(nil).group(:state).count
+ end
+
def predefined?(milestone)
milestone == Any ||
milestone == None ||
@@ -129,18 +146,29 @@ class Milestone < ActiveRecord::Base
@link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
- def self.upcoming_ids_by_projects(projects)
- rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now)
+ def self.upcoming_ids(projects, groups)
+ rel = unscoped
+ .for_projects_and_groups(projects, groups)
+ .active.where('milestones.due_date > NOW()')
if Gitlab::Database.postgresql?
- rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
+ rel.order(:project_id, :group_id, :due_date).select('DISTINCT ON (project_id, group_id) id')
else
+ # We need to use MySQL's NULL-safe comparison operator `<=>` here
+ # because one of `project_id` or `group_id` is always NULL
+ join_clause = <<~HEREDOC
+ LEFT OUTER JOIN milestones earlier_milestones
+ ON milestones.project_id <=> earlier_milestones.project_id
+ AND milestones.group_id <=> earlier_milestones.group_id
+ AND milestones.due_date > earlier_milestones.due_date
+ AND earlier_milestones.due_date > NOW()
+ AND earlier_milestones.state = 'active'
+ HEREDOC
+
rel
- .group(:project_id, :due_date, :id)
- .having('due_date = MIN(due_date)')
- .pluck(:id, :project_id, :due_date)
- .uniq(&:second)
- .map(&:first)
+ .joins(join_clause)
+ .where('earlier_milestones.id IS NULL')
+ .select(:id)
end
end
@@ -174,7 +202,7 @@ class Milestone < ActiveRecord::Base
return STATE_COUNT_HASH unless projects || groups
counts = Milestone
- .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id))
+ .for_projects_and_groups(projects, groups)
.reorder(nil)
.group(:state)
.count
@@ -212,10 +240,10 @@ class Milestone < ActiveRecord::Base
end
def reference_link_text(from = nil)
- self.title
+ self.class.reference_prefix + self.title
end
- def milestoneish_ids
+ def milestoneish_id
id
end
@@ -258,8 +286,7 @@ class Milestone < ActiveRecord::Base
if project
relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
elsif group
- project_ids = group.projects.map(&:id)
- relation = Milestone.for_projects_and_groups(project_ids, [group.id])
+ relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id])
end
title_exists = relation.find_by_title(title)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a0bebc5e9a2..a5c479bdc0c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Namespace < ActiveRecord::Base
+class Namespace < ApplicationRecord
include CacheMarkdownField
include Sortable
include Gitlab::VisibilityLevel
@@ -149,7 +149,7 @@ class Namespace < ActiveRecord::Base
end
def find_fork_of(project)
- return nil unless project.fork_network
+ return unless project.fork_network
if Gitlab::SafeRequestStore.active?
forks_in_namespace = Gitlab::SafeRequestStore.fetch("namespaces:#{id}:forked_projects") do
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 6da3bb7bfb7..ecbeb24ee0a 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -40,9 +40,12 @@ module Network
# Get commits from repository
#
def collect_commits
- find_commits(count_to_display_commit_in_center).map do |commit|
- # Decorate with app/model/network/commit.rb
- Network::Commit.new(commit)
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/58013
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ find_commits(count_to_display_commit_in_center).map do |commit|
+ # Decorate with app/model/network/commit.rb
+ Network::Commit.new(commit)
+ end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index becf14e9785..1578ae9c4cc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -456,6 +456,10 @@ class Note < ActiveRecord::Base
Upload.find_by(model: self, path: paths)
end
+ def parent
+ project
+ end
+
private
def keep_around_commit
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
index 27aef7adc48..e369122003e 100644
--- a/app/models/note_diff_file.rb
+++ b/app/models/note_diff_file.rb
@@ -3,7 +3,22 @@
class NoteDiffFile < ActiveRecord::Base
include DiffFile
+ scope :for_commit_or_unresolved, -> do
+ joins(:diff_note).where("resolved_at IS NULL OR noteable_type = 'Commit'")
+ end
+
+ delegate :original_position, :project, to: :diff_note
+
belongs_to :diff_note, inverse_of: :note_diff_file
validates :diff_note, presence: true
+
+ def raw_diff_file
+ raw_diff = Gitlab::Git::Diff.new(to_hash)
+
+ Gitlab::Diff::File.new(raw_diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs,
+ unique_identifier: id)
+ end
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 9f16eefe074..793cce191fa 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -119,7 +119,7 @@ class NotificationRecipient
private
def read_ability
- return nil if @skip_read_ability
+ return if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability =
@@ -136,7 +136,7 @@ class NotificationRecipient
end
def default_project
- return nil if @target.nil?
+ return if @target.nil?
return @target if @target.is_a?(Project)
return @target.project if @target.respond_to?(:project)
end
@@ -153,7 +153,7 @@ class NotificationRecipient
user.global_notification_setting
end
- # Returns the notificaton_setting of the lowest group in hierarchy with non global level
+ # Returns the notification_setting of the lowest group in hierarchy with non global level
def closest_non_global_group_notification_settting
return unless @group
return if indexed_group_notification_settings.empty?
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 73a58f2420e..ed78a46eaf3 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -2,8 +2,11 @@
class PersonalAccessToken < ActiveRecord::Base
include Expirable
+ include IgnorableColumn
include TokenAuthenticatable
- add_authentication_token_field :token, digest: true, fallback: true
+
+ add_authentication_token_field :token, digest: true
+ ignore_column :token
REDIS_EXPIRY_TIME = 3.minutes
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index ad6a008dee8..4635fc72dc7 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -85,14 +85,20 @@ class PoolRepository < ActiveRecord::Base
def unlink_repository(repository)
object_pool.unlink_repository(repository.raw)
- mark_obsolete unless member_projects.where.not(id: repository.project.id).exists?
+ if member_projects.where.not(id: repository.project.id).exists?
+ true
+ else
+ mark_obsolete
+ end
end
def object_pool
@object_pool ||= Gitlab::Git::ObjectPool.new(
shard.name,
disk_path + '.git',
- source_project.repository.raw)
+ source_project.repository.raw,
+ source_project.full_path
+ )
end
def inspect
diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb
index 0e667dac21e..5f0f313b7f9 100644
--- a/app/models/programming_language.rb
+++ b/app/models/programming_language.rb
@@ -3,4 +3,10 @@
class ProgrammingLanguage < ActiveRecord::Base
validates :name, presence: true
validates :color, allow_blank: false, color: true
+
+ # Returns all programming languages which match the given name (case
+ # insensitively).
+ scope :with_name_case_insensitive, ->(name) do
+ where(arel_table[:name].matches(sanitize_sql_like(name)))
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 09e2a6114fe..4cc13f372c1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -73,7 +73,7 @@ class Project < ActiveRecord::Base
delegate :no_import?, to: :import_state, allow_nil: true
default_value_for :archived, false
- default_value_for :visibility_level, gitlab_config_features.visibility_level
+ default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility }
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
@@ -85,7 +85,7 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- add_authentication_token_field :runners_token, encrypted: true, migrating: true
+ add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required }
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@@ -147,7 +147,6 @@ class Project < ActiveRecord::Base
has_one :pipelines_email_service
has_one :irker_service
has_one :pivotaltracker_service
- has_one :hipchat_service
has_one :flowdock_service
has_one :assembla_service
has_one :asana_service
@@ -161,6 +160,7 @@ class Project < ActiveRecord::Base
has_one :pushover_service
has_one :jira_service
has_one :redmine_service
+ has_one :youtrack_service
has_one :custom_issue_tracker_service
has_one :bugzilla_service
has_one :gitlab_issue_tracker_service, inverse_of: :project
@@ -187,6 +187,7 @@ class Project < ActiveRecord::Base
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project
+ has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -248,10 +249,10 @@ class Project < ActiveRecord::Base
has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :commit_statuses
- # The relation :all_pipelines is intented to be used when we want to get the
+ # The relation :all_pipelines is intended to be used when we want to get the
# whole list of pipelines associated to the project
has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
- # The relation :ci_pipelines is intented to be used when we want to get only
+ # The relation :ci_pipelines is intended to be used when we want to get only
# those pipeline which are directly related to CI. There are
# other pipelines, like webide ones, that we won't retrieve
# if we use this relation.
@@ -295,6 +296,8 @@ class Project < ActiveRecord::Base
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+ accepts_nested_attributes_for :error_tracking_setting, update_only: true
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
@@ -303,6 +306,7 @@ class Project < ActiveRecord::Base
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
delegate :group_clusters_enabled?, to: :group, allow_nil: true
delegate :root_ancestor, to: :namespace, allow_nil: true
+ delegate :last_pipeline, to: :commit, allow_nil: true
# Validations
validates :creator, presence: true, on: :create
@@ -324,15 +328,14 @@ class Project < ActiveRecord::Base
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
- validates :import_url, url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
- ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
- allow_localhost: false,
- enforce_user: true }, if: [:external_import?, :import_url_changed?]
+ validates :import_url, public_url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
+ ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
+ enforce_user: true }, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
- validate :check_limit, on: :create
+ validate :check_personal_projects_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
- validate :visibility_level_allowed_by_group
- validate :visibility_level_allowed_as_fork
+ validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
+ validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
validate :check_wiki_path_conflict
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
@@ -375,8 +378,10 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
- access_level_attribute = ProjectFeature.access_level_attribute(feature)
- with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
+ access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
+ enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
+
+ with_project_feature.where(enabled_feature)
}
# Picks a feature where the level is exactly that given.
@@ -385,6 +390,16 @@ class Project < ActiveRecord::Base
with_project_feature.where(project_features: { access_level_attribute => level })
}
+ # Picks projects which use the given programming language
+ scope :with_programming_language, ->(language_name) do
+ lang_id_query = ProgrammingLanguage
+ .with_name_case_insensitive(language_name)
+ .select(:id)
+
+ joins(:repository_languages)
+ .where(repository_languages: { programming_language_id: lang_id_query })
+ end
+
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
@@ -463,7 +478,8 @@ class Project < ActiveRecord::Base
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
- visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin?
with_feature_enabled(feature)
@@ -471,10 +487,15 @@ class Project < ActiveRecord::Base
column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature
- .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
- visible,
- ProjectFeature::PRIVATE,
- user.authorizations_for_projects)
+ .where(
+ "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
+ " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
+ {
+ private: Gitlab::VisibilityLevel::PRIVATE,
+ public_visible: ProjectFeature::ENABLED,
+ private_visible: ProjectFeature::PRIVATE,
+ authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
+ })
else
with_feature_access_level(feature, visible)
end
@@ -528,6 +549,7 @@ class Project < ActiveRecord::Base
def reference_pattern
%r{
+ (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
@@ -567,6 +589,22 @@ class Project < ActiveRecord::Base
end
end
+ def all_pipelines
+ if builds_enabled?
+ super
+ else
+ super.external
+ end
+ end
+
+ def ci_pipelines
+ if builds_enabled?
+ super
+ else
+ super.external
+ end
+ end
+
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
@@ -601,6 +639,10 @@ class Project < ActiveRecord::Base
auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
end
+ def daily_statistics_enabled?
+ Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
+ end
+
def empty_repo?
repository.empty?
end
@@ -645,19 +687,15 @@ class Project < ActiveRecord::Base
end
# ref can't be HEAD, can only be branch/tag name or SHA
- def latest_successful_builds_for(ref = default_branch)
+ def latest_successful_build_for(job_name, ref = default_branch)
latest_pipeline = ci_pipelines.latest_successful_for(ref)
+ return unless latest_pipeline
- if latest_pipeline
- latest_pipeline.builds.latest.with_artifacts_archive
- else
- builds.none
- end
+ latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
end
- def latest_successful_build_for(job_name, ref = default_branch)
- builds = latest_successful_builds_for(ref)
- builds.find_by!(name: job_name)
+ def latest_successful_build_for!(job_name, ref = default_branch)
+ latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
end
def merge_base_commit(first_commit_id, second_commit_id)
@@ -723,11 +761,13 @@ class Project < ActiveRecord::Base
end
def import_url=(value)
- return super(value) unless Gitlab::UrlSanitizer.valid?(value)
-
- import_url = Gitlab::UrlSanitizer.new(value)
- super(import_url.sanitized_url)
- create_or_update_import_data(credentials: import_url.credentials)
+ if Gitlab::UrlSanitizer.valid?(value)
+ import_url = Gitlab::UrlSanitizer.new(value)
+ super(import_url.sanitized_url)
+ create_or_update_import_data(credentials: import_url.credentials)
+ else
+ super(value)
+ end
end
def import_url
@@ -811,18 +851,22 @@ class Project < ActiveRecord::Base
::Gitlab::CurrentSettings.mirror_available
end
- def check_limit
- unless creator.can_create_project? || namespace.kind == 'group'
- projects_limit = creator.projects_limit
+ def check_personal_projects_limit
+ # Since this method is called as validation hook, `creator` might not be
+ # present. Since the validation for that will fail, we can just return
+ # early.
+ return if !creator || creator.can_create_project? ||
+ namespace.kind == 'group'
- if projects_limit == 0
- self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
+ limit = creator.projects_limit
+ error =
+ if limit.zero?
+ _('Personal project creation is not allowed. Please contact your administrator with questions')
else
- self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
+ _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
end
- end
- rescue
- self.errors.add(:base, "Can't check your ability to create project")
+
+ self.errors.add(:limit_reached, error % { limit: limit })
end
def visibility_level_allowed_by_group
@@ -913,11 +957,16 @@ class Project < ActiveRecord::Base
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
+ # check since this can come from a request parameter
+ return unless %w(issue merge_request).include?(address_type)
+
author.ensure_incoming_email_token!
- suffix = address_type == 'merge_request' ? '+merge-request' : ''
- Gitlab::IncomingEmail.reply_address(
- "#{full_path}#{suffix}+#{author.incoming_email_token}")
+ suffix = address_type.dasherize
+
+ # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue@localhost.com
+ # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-merge-request@localhost.com
+ Gitlab::IncomingEmail.reply_address("#{full_path_slug}-#{project_id}-#{author.incoming_email_token}-#{suffix}")
end
def build_commit_note(commit)
@@ -1042,7 +1091,7 @@ class Project < ActiveRecord::Base
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
- params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
@@ -1171,7 +1220,7 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
- # Is overriden in EE
+ # Is overridden in EE
def lfs_http_url_to_repo(_)
http_url_to_repo
end
@@ -1181,7 +1230,7 @@ class Project < ActiveRecord::Base
end
def fork_source
- return nil unless forked?
+ return unless forked?
forked_from_project || fork_network&.root_project
end
@@ -1252,7 +1301,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously
return if forked? && !force
- if gitlab_shell.create_repository(repository_storage, disk_path)
+ if gitlab_shell.create_project_repository(self)
repository.after_create
true
else
@@ -1528,7 +1577,7 @@ class Project < ActiveRecord::Base
end
def pages_available?
- Gitlab.config.pages.enabled && !namespace.subgroup?
+ Gitlab.config.pages.enabled
end
def remove_private_deploy_keys
@@ -1578,6 +1627,13 @@ class Project < ActiveRecord::Base
def after_import
repository.after_import
wiki.repository.after_import
+
+ # The import assigns iid values on its own, e.g. by re-using GitHub ids.
+ # Flush existing InternalId records for this project for consistency reasons.
+ # Those records are going to be recreated with the next normal creation
+ # of a model instance (e.g. an Issue).
+ InternalId.flush_records!(project: self)
+
import_state.finish
import_state.remove_jid
update_project_counter_caches
@@ -1599,24 +1655,7 @@ class Project < ActiveRecord::Base
# rubocop: disable CodeReuse/ServiceClass
def after_create_default_branch
- return unless default_branch
-
- # Ensure HEAD points to the default branch in case it is not master
- change_head(default_branch)
-
- if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
- params = {
- name: default_branch,
- push_access_levels_attributes: [{
- access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
- }],
- merge_access_levels_attributes: [{
- access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER
- }]
- }
-
- ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
- end
+ Projects::ProtectDefaultBranchService.new(self).execute
end
# rubocop: enable CodeReuse/ServiceClass
@@ -1640,7 +1679,7 @@ class Project < ActiveRecord::Base
end
def export_path
- return nil unless namespace.present? || hashed_storage?(:repository)
+ return unless namespace.present? || hashed_storage?(:repository)
import_export_shared.archive_path
end
@@ -1699,8 +1738,23 @@ class Project < ActiveRecord::Base
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
+ .concat(pages_variables)
.concat(container_registry_variables)
.concat(auto_devops_variables)
+ .concat(api_variables)
+ end
+
+ def pages_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host)
+ variables.append(key: 'CI_PAGES_URL', value: pages_url)
+ end
+ end
+
+ def api_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url)
+ end
end
def container_registry_variables
@@ -1734,10 +1788,21 @@ class Project < ActiveRecord::Base
end
def protected_for?(ref)
- if repository.branch_exists?(ref)
- ProtectedBranch.protected?(self, ref)
- elsif repository.tag_exists?(ref)
- ProtectedTag.protected?(self, ref)
+ raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
+
+ resolved_ref = repository.expand_ref(ref) || ref
+ return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)
+
+ ref_name = if resolved_ref == ref
+ Gitlab::Git.ref_name(resolved_ref)
+ else
+ ref
+ end
+
+ if Gitlab::Git.branch_ref?(resolved_ref)
+ ProtectedBranch.protected?(self, ref_name)
+ elsif Gitlab::Git.tag_ref?(resolved_ref)
+ ProtectedTag.protected?(self, ref_name)
end
end
@@ -1764,6 +1829,24 @@ class Project < ActiveRecord::Base
handle_update_attribute_error(e, value)
end
+ # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
+ #
+ # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
+ def set_repository_read_only!
+ with_lock do
+ break false if git_transfer_in_progress?
+
+ update_column(:repository_read_only, true)
+ end
+ end
+
+ # Set repository as writable again
+ def set_repository_writable!
+ with_lock do
+ update_column(:repository_read_only, false)
+ end
+ end
+
def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end
@@ -1847,6 +1930,14 @@ class Project < ActiveRecord::Base
persisted? && path_changed?
end
+ def human_merge_method
+ if merge_method == :ff
+ 'Fast-forward'
+ else
+ merge_method.to_s.humanize
+ end
+ end
+
def merge_method
if self.merge_requests_ff_only_enabled
:ff
@@ -1878,15 +1969,27 @@ class Project < ActiveRecord::Base
def migrate_to_hashed_storage!
return unless storage_upgradable?
- update!(repository_read_only: true)
+ if git_transfer_in_progress?
+ HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
+ else
+ HashedStorage::ProjectMigrateWorker.perform_async(id)
+ end
+ end
+
+ def rollback_to_legacy_storage!
+ return if legacy_storage?
- if repo_reference_count > 0 || wiki_reference_count > 0
- ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
+ if git_transfer_in_progress?
+ HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
else
- ProjectMigrateHashedStorageWorker.perform_async(id)
+ HashedStorage::ProjectRollbackWorker.perform_async(id)
end
end
+ def git_transfer_in_progress?
+ repo_reference_count > 0 || wiki_reference_count > 0
+ end
+
def storage_version=(value)
super
@@ -1918,23 +2021,15 @@ class Project < ActiveRecord::Base
.where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1)
.select(1)
- source_of_merge_requests.opened
- .where(allow_collaboration: true)
- .where('EXISTS (?)', developer_access_exists)
+ merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
end
- def branch_allows_collaboration?(user, branch_name)
- return false unless user
-
- cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
-
- memoized_results = strong_memoize(:branch_allows_collaboration) do
- Hash.new do |result, cache_key|
- result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name)
- end
- end
+ def any_branch_allows_collaboration?(user)
+ fetch_branch_allows_collaboration(user)
+ end
- memoized_results[cache_key]
+ def branch_allows_collaboration?(user, branch_name)
+ fetch_branch_allows_collaboration(user, branch_name)
end
def licensed_features
@@ -2003,11 +2098,25 @@ class Project < ActiveRecord::Base
end
def leave_pool_repository
- pool_repository&.unlink_repository(repository)
+ pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil)
+ end
+
+ def link_pool_repository
+ pool_repository&.link_repository(repository)
+ end
+
+ def has_pool_repository?
+ pool_repository.present?
end
private
+ def merge_requests_allowing_collaboration(source_branch = nil)
+ relation = source_of_merge_requests.opened.where(allow_collaboration: true)
+ relation = relation.where(source_branch: source_branch) if source_branch
+ relation
+ end
+
def create_new_pool_repository
pool = begin
create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
@@ -2132,26 +2241,19 @@ class Project < ActiveRecord::Base
raise ex
end
- def fetch_branch_allows_collaboration?(user, branch_name)
- check_access = -> do
- next false if empty_repo?
+ def fetch_branch_allows_collaboration(user, branch_name = nil)
+ return false unless user
- merge_requests = source_of_merge_requests.opened
- .where(allow_collaboration: true)
+ Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
+ next false if empty_repo?
# Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322
Gitlab::GitalyClient.allow_n_plus_1_calls do
- if branch_name
- merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user)
- else
- merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) }
+ merge_requests_allowing_collaboration(branch_name).any? do |merge_request|
+ merge_request.can_be_merged_by?(user)
end
end
end
-
- Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
- check_access.call
- end
end
def services_templates
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 2253ad7b543..e353a6443c4 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -24,6 +24,12 @@ class ProjectAutoDevops < ActiveRecord::Base
domain.present? || instance_domain.present?
end
+ # From 11.8, AUTO_DEVOPS_DOMAIN has been replaced by KUBE_INGRESS_BASE_DOMAIN.
+ # See Clusters::Cluster#predefined_variables and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580
+ # for more info.
+ #
+ # Suppport AUTO_DEVOPS_DOMAIN is scheduled to be removed on
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/52363
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
if has_domain?
diff --git a/app/models/project_daily_statistic.rb b/app/models/project_daily_statistic.rb
new file mode 100644
index 00000000000..ff115dd010f
--- /dev/null
+++ b/app/models/project_daily_statistic.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class ProjectDailyStatistic < ActiveRecord::Base
+ belongs_to :project
+
+ scope :of_project, -> (project) { where(project: project) }
+ scope :of_last_30_days, -> { where('date >= ?', 29.days.ago.utc.to_date) }
+ scope :sorted_by_date_desc, -> { order(project_id: :desc, date: :desc) }
+ scope :sum_fetch_count, -> { sum(:fetch_count) }
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 39f2b8fe0de..e6787236c4e 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base
PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
class << self
def access_level_attribute(feature)
- feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
- raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+ feature = ensure_feature!(feature)
"#{feature}_access_level".to_sym
end
@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base
"#{table}.#{attribute}"
end
+
+ def required_minimum_access_level(feature)
+ feature = ensure_feature!(feature)
+
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
+ end
+
+ private
+
+ def ensure_feature!(feature)
+ feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+ raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+ feature
+ end
end
# Default scopes force us to unscope here since a service may need to check
@@ -61,7 +76,7 @@ class ProjectFeature < ActiveRecord::Base
# This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true)
- get_permission(user, access_level(feature))
+ get_permission(user, feature)
end
def access_level(feature)
@@ -119,12 +134,12 @@ class ProjectFeature < ActiveRecord::Base
(FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
end
- def get_permission(user, level)
- case level
+ def get_permission(user, feature)
+ case access_level(feature)
when DISABLED
false
when PRIVATE
- user && (project.team.member?(user) || user.full_private_access?)
+ team_access?(user, feature)
when ENABLED
true
when PUBLIC
@@ -133,4 +148,11 @@ class ProjectFeature < ActiveRecord::Base
true
end
end
+
+ def team_access?(user, feature)
+ return unless user
+ return true if user.full_private_access?
+
+ project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
+ end
end
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 525725034a5..aa0c121fe99 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -30,4 +30,8 @@ class ProjectImportData < ActiveRecord::Base
def merge_credentials(hash)
self.credentials = credentials.to_h.merge(hash) unless hash.empty?
end
+
+ def clear_credentials
+ self.credentials = {}
+ end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index a252052200a..71f5607dbdb 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -80,19 +80,27 @@ class BambooService < CiService
private
- def get_build_result_index
- # When Bamboo returns multiple results for a given changeset, arbitrarily assume the most relevant result to be the last one.
- -1
+ def get_build_result(response)
+ return if response.code != 200
+
+ # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
+ result = response.dig('results', 'results', 'result')
+
+ # In case of multiple results, arbitrarily assume the last one is the most relevant.
+ return result.last if result.is_a?(Array)
+
+ result
end
def read_build_page(response)
+ result = get_build_result(response)
key =
- if response.code != 200 || response.dig('results', 'results', 'size') == '0'
+ if result.blank?
# If actual build link can't be determined, send user to build summary page.
build_key
else
# If actual build link is available, go to build result page.
- response.dig('results', 'results', 'result', get_build_result_index, 'planResultKey', 'key')
+ result.dig('planResultKey', 'key')
end
build_url("browse/#{key}")
@@ -101,11 +109,15 @@ class BambooService < CiService
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
- status = if response.code == 404 || response.dig('results', 'results', 'size') == '0'
- 'Pending'
- else
- response.dig('results', 'results', 'result', get_build_result_index, 'buildState')
- end
+ result = get_build_result(response)
+ status =
+ if result.blank?
+ 'Pending'
+ else
+ result.dig('buildState')
+ end
+
+ return :error unless status.present?
if status.include?('Success')
'success'
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 1d7877a1fb5..ad26e42a21b 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -57,7 +57,7 @@ class CampfireService < Service
# https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
def speak(room_name, message, auth)
room = rooms(auth).find { |r| r["name"] == room_name }
- return nil unless room
+ return unless room
path = "/room/#{room["id"]}/speak.json"
body = {
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index 6dae4f3a4a6..80aa2101509 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -11,7 +11,7 @@ class DeploymentService < Service
%w()
end
- def predefined_variables
+ def predefined_variables(project:)
[]
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
deleted file mode 100644
index a69b7b4c4b6..00000000000
--- a/app/models/project_services/hipchat_service.rb
+++ /dev/null
@@ -1,311 +0,0 @@
-# frozen_string_literal: true
-
-class HipchatService < Service
- include ActionView::Helpers::SanitizeHelper
-
- MAX_COMMITS = 3
- HIPCHAT_ALLOWED_TAGS = %w[
- 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_pipelines, :notify
- validates :token, presence: true, if: :activated?
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_pipelines = true
- end
- end
-
- def title
- 'HipChat'
- end
-
- def description
- 'Private group chat and IM'
- end
-
- def self.to_param
- 'hipchat'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: 'Room token', required: true },
- { type: 'text', name: 'room', placeholder: 'Room name or ID' },
- { type: 'checkbox', name: 'notify' },
- { 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_pipelines' }
- ]
- end
-
- def self.supported_events
- %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- message = create_message(data)
- return unless message.present?
-
- gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def test(data)
- begin
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
- private
-
- def gate
- options = { api_version: api_version.present? ? api_version : 'v2' }
- options[:server_url] = server unless server.blank?
- @gate ||= HipChat::Client.new(token, options)
- end
-
- def message_options(data = nil)
- { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
- end
-
- def create_message(data)
- object_kind = data[:object_kind]
-
- case object_kind
- when "push", "tag_push"
- create_push_message(data)
- when "issue"
- create_issue_message(data) unless update?(data)
- when "merge_request"
- create_merge_request_message(data) unless update?(data)
- when "note"
- create_note_message(data)
- when "pipeline"
- create_pipeline_message(data) if should_pipeline_be_notified?(data)
- end
- end
-
- def render_line(text)
- markdown(text.lines.first.chomp, pipeline: :single_line) if text
- end
-
- def create_push_message(push)
- ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
- ref = Gitlab::Git.ref_name(push[:ref])
-
- before = push[:before]
- after = push[:after]
-
- message = []
- message << "#{push[:user_name]} "
-
- if Gitlab::Git.blank_ref?(before)
- message << "pushed new #{ref_type} <a href=\""\
- "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\
- " to #{project_link}\n"
- elsif Gitlab::Git.blank_ref?(after)
- message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
- else
- message << "pushed to #{ref_type} <a href=\""\
- "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
- message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> "
- message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
-
- push[:commits].take(MAX_COMMITS).each do |commit|
- message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
- end
-
- if push[:commits].count > MAX_COMMITS
- message << "<br />... #{push[:commits].count - MAX_COMMITS} more commits"
- end
- end
-
- message.join
- end
-
- def markdown(text, options = {})
- return "" unless text
-
- context = {
- project: project,
- pipeline: :email
- }
-
- Banzai.render(text, context)
-
- context.merge!(options)
-
- html = Banzai.render_and_post_process(text, context)
- sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
-
- sanitized_html.truncate(200, separator: ' ', omission: '...')
- end
-
- def create_issue_message(data)
- user_name = data[:user][:name]
-
- obj_attr = data[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- title = render_line(obj_attr[:title])
- state = obj_attr[:state]
- issue_iid = obj_attr[:iid]
- issue_url = obj_attr[:url]
- description = obj_attr[:description]
-
- issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>"
-
- message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"]
- message << "<pre>#{markdown(description)}</pre>"
-
- message.join
- end
-
- def create_merge_request_message(data)
- user_name = data[:user][:name]
-
- obj_attr = data[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- merge_request_id = obj_attr[:iid]
- state = obj_attr[:state]
- description = obj_attr[:description]
- title = render_line(obj_attr[:title])
-
- merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
- merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
- message = ["#{user_name} #{state} #{merge_request_link} in " \
- "#{project_link}: <b>#{title}</b>"]
-
- message << "<pre>#{markdown(description)}</pre>"
- message.join
- end
-
- def format_title(title)
- "<b>#{render_line(title)}</b>"
- end
-
- def create_note_message(data)
- data = HashWithIndifferentAccess.new(data)
- user_name = data[:user][:name]
-
- obj_attr = HashWithIndifferentAccess.new(data[:object_attributes])
- note = obj_attr[:note]
- note_url = obj_attr[:url]
- noteable_type = obj_attr[:noteable_type]
- commit_id = nil
-
- case noteable_type
- when "Commit"
- commit_attr = HashWithIndifferentAccess.new(data[:commit])
- commit_id = commit_attr[:id]
- subject_desc = commit_id
- subject_desc = Commit.truncate_sha(subject_desc)
- subject_type = "commit"
- title = format_title(commit_attr[:message])
- when "Issue"
- subj_attr = HashWithIndifferentAccess.new(data[:issue])
- subject_id = subj_attr[:iid]
- subject_desc = "##{subject_id}"
- subject_type = "issue"
- title = format_title(subj_attr[:title])
- when "MergeRequest"
- subj_attr = HashWithIndifferentAccess.new(data[:merge_request])
- subject_id = subj_attr[:iid]
- subject_desc = "!#{subject_id}"
- subject_type = "merge request"
- title = format_title(subj_attr[:title])
- when "Snippet"
- subj_attr = HashWithIndifferentAccess.new(data[:snippet])
- subject_id = subj_attr[:id]
- subject_desc = "##{subject_id}"
- subject_type = "snippet"
- title = format_title(subj_attr[:title])
- end
-
- subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>"
- message = ["#{user_name} commented on #{subject_html} in #{project_link}: "]
- message << title
-
- message << "<pre>#{markdown(note, ref: commit_id)}</pre>"
- message.join
- end
-
- 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>"
- pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
-
- "#{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)
- pipeline_status_color(data) || color || 'yellow'
- end
-
- def pipeline_status_color(data)
- return unless data && data[:object_kind] == 'pipeline'
-
- case data[:object_attributes][:status]
- when 'success'
- 'green'
- else
- 'red'
- end
- end
-
- def project_name
- project.full_name.gsub(/\s/, '')
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "<a href=\"#{project_url}\">#{project_name}</a>"
- end
-
- def update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def humanized_status(status)
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def should_pipeline_be_notified?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
- end
-end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a15780c14f9..fb76bc89c98 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -59,7 +59,7 @@ class IrkerService < Service
' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
' want to use a password, you have to omit the "#" on the channel). If you ' \
' specify a default IRC URI to prepend before each recipient, you can just ' \
- ' give a channel name.' },
+ ' give a channel name.' },
{ type: 'checkbox', name: 'colorize_messages' }
]
end
@@ -112,7 +112,7 @@ class IrkerService < Service
end
def consider_uri(uri)
- return nil if uri.scheme.nil?
+ return if uri.scheme.nil?
# Authorize both irc://domain.com/#chan and irc://domain.com/chan
if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 9066a0b7f1d..f7064d5aaea 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -215,7 +215,7 @@ class JiraService < IssueTrackerService
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- link_title = "GitLab: Solved by commit #{commit_id}."
+ link_title = "Solved by commit #{commit_id}."
comment = "Issue solved with [#{commit_id}|#{commit_url}]."
link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
send_message(issue, comment, link_props)
@@ -230,7 +230,7 @@ class JiraService < IssueTrackerService
project_name = data[:project][:name]
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
- link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
unless comment_exists?(issue, message)
@@ -278,6 +278,7 @@ class JiraService < IssueTrackerService
{
GlobalID: 'GitLab',
+ relationship: 'mentioned on',
object: {
url: url,
title: title,
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index b801fd84a07..f69edd60003 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -53,7 +53,7 @@ class KubernetesService < DeploymentService
end
def description
- 'Kubernetes / Openshift integration'
+ 'Kubernetes / OpenShift integration'
end
def help
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 60cb2d380d5..c68a9d923c8 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -71,7 +71,7 @@ class PrometheusService < MonitoringService
end
def prometheus_client
- RestClient::Resource.new(api_url, max_redirects: 0) if api_url && manual_configuration? && active?
+ RestClient::Resource.new(api_url, max_redirects: 0) if should_return_client?
end
def prometheus_available?
@@ -83,6 +83,10 @@ class PrometheusService < MonitoringService
private
+ def should_return_client?
+ api_url && manual_configuration? && active? && valid?
+ end
+
def synchronize_service_state
self.active = prometheus_available? || manual_configuration?
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index 6c82e088231..6a454070fe2 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -22,6 +22,10 @@ class SlackSlashCommandsService < SlashCommandsService
end
end
+ def chat_responder
+ ::Gitlab::Chat::Responder::Slack
+ end
+
private
def format(text)
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b8e17087db5..3245cd22e73 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -39,9 +39,7 @@ class TeamcityService < CiService
end
def help
- 'The build configuration in Teamcity must use the build format '\
- 'number %build.vcs.number% '\
- 'you will also want to configure monitoring of all branches so merge '\
+ 'You will want to configure monitoring of all branches so merge '\
'requests build, that setting is in the vsc root advanced settings.'
end
@@ -70,7 +68,7 @@ class TeamcityService < CiService
end
def calculate_reactive_cache(sha, ref)
- response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
+ response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
new file mode 100644
index 00000000000..957be685aea
--- /dev/null
+++ b/app/models/project_services/youtrack_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class YoutrackService < IssueTrackerService
+ validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
+
+ prop_accessor :description, :project_url, :issues_url
+
+ # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1
+ def self.reference_pattern(only_long: false)
+ if only_long
+ /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)/
+ else
+ /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/
+ end
+ end
+
+ def title
+ 'YouTrack'
+ end
+
+ def description
+ if self.properties && self.properties['description'].present?
+ self.properties['description']
+ else
+ 'YouTrack issue tracker'
+ end
+ end
+
+ def self.to_param
+ 'youtrack'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'description', placeholder: description },
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true }
+ ]
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33bc6a561f9..aeba2843e5d 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -74,6 +74,14 @@ class ProjectTeam
end
alias_method :users, :members
+ # `members` method uses project_authorizations table which
+ # is updated asynchronously, on project move it still contains
+ # old members who may not have access to the new location,
+ # so we filter out only members of project or project's group
+ def members_in_project_and_ancestors
+ members.where(id: member_user_ids)
+ end
+
def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
@@ -191,4 +199,8 @@ class ProjectTeam
def group
project.group
end
+
+ def member_user_ids
+ Member.on_project_and_ancestors(project).select(:user_id)
+ end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 559e4f99294..c43bd45a62f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -60,7 +60,7 @@ class ProjectWiki
def wiki
@wiki ||= begin
gl_repository = Gitlab::GlRepository.gl_repository(project, true)
- raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
+ raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path)
create_repo!(raw_repository) unless raw_repository.exists?
@@ -175,7 +175,7 @@ class ProjectWiki
private
def create_repo!(raw_repository)
- gitlab_shell.create_repository(project.repository_storage, disk_path)
+ gitlab_shell.create_wiki_repository(project)
raise CouldNotCreateWikiError unless raw_repository.exists?
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index defbade1ed6..5594594a48d 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -18,6 +18,54 @@ class PrometheusMetric < ActiveRecord::Base
system: 2
}
+ GROUP_DETAILS = {
+ # built-in groups
+ nginx_ingress_vts: {
+ group_title: _('Response metrics (NGINX Ingress VTS)'),
+ required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ priority: 10
+ }.freeze,
+ nginx_ingress: {
+ group_title: _('Response metrics (NGINX Ingress)'),
+ required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
+ priority: 10
+ }.freeze,
+ ha_proxy: {
+ group_title: _('Response metrics (HA Proxy)'),
+ required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ priority: 10
+ }.freeze,
+ aws_elb: {
+ group_title: _('Response metrics (AWS ELB)'),
+ required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ priority: 10
+ }.freeze,
+ nginx: {
+ group_title: _('Response metrics (NGINX)'),
+ required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
+ priority: 10
+ }.freeze,
+ kubernetes: {
+ group_title: _('System metrics (Kubernetes)'),
+ required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ priority: 5
+ }.freeze,
+
+ # custom/user groups
+ business: {
+ group_title: _('Business metrics (Custom)'),
+ priority: 0
+ }.freeze,
+ response: {
+ group_title: _('Response metrics (Custom)'),
+ priority: -5
+ }.freeze,
+ system: {
+ group_title: _('System metrics (Custom)'),
+ priority: -10
+ }.freeze
+ }.freeze
+
validates :title, presence: true
validates :query, presence: true
validates :group, presence: true
@@ -29,36 +77,16 @@ class PrometheusMetric < ActiveRecord::Base
scope :common, -> { where(common: true) }
- GROUP_TITLES = {
- # built-in groups
- nginx_ingress_vts: _('Response metrics (NGINX Ingress VTS)'),
- nginx_ingress: _('Response metrics (NGINX Ingress)'),
- ha_proxy: _('Response metrics (HA Proxy)'),
- aws_elb: _('Response metrics (AWS ELB)'),
- nginx: _('Response metrics (NGINX)'),
- kubernetes: _('System metrics (Kubernetes)'),
-
- # custom/user groups
- business: _('Business metrics (Custom)'),
- response: _('Response metrics (Custom)'),
- system: _('System metrics (Custom)')
- }.freeze
-
- REQUIRED_METRICS = {
- nginx_ingress_vts: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
- nginx_ingress: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
- ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
- aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
- nginx: %w(nginx_server_requests nginx_server_requestMsec),
- kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
- }.freeze
+ def priority
+ group_details(group).fetch(:priority)
+ end
def group_title
- GROUP_TITLES[group.to_sym]
+ group_details(group).fetch(:group_title)
end
def required_metrics
- REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s)
+ group_details(group).fetch(:required_metrics, []).map(&:to_s)
end
def to_query_metric
@@ -89,4 +117,10 @@ class PrometheusMetric < ActiveRecord::Base
}]
end
end
+
+ private
+
+ def group_details(group)
+ GROUP_DETAILS.fetch(group.to_sym)
+ end
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index d075440b147..597431be65a 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -18,13 +18,23 @@ class ProtectedBranch < ActiveRecord::Base
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
- refs = project.protected_branches.select(:name)
+ self.matching(ref_name, protected_refs: protected_refs(project)).present?
+ end
- self.matching(ref_name, protected_refs: refs).present?
+ def self.any_protected?(project, ref_names)
+ protected_refs(project).any? do |protected_ref|
+ ref_names.any? do |ref_name|
+ protected_ref.matches?(ref_name)
+ end
+ end
end
def self.default_branch_protected?
Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
+
+ def self.protected_refs(project)
+ project.protected_branches.select(:name)
+ end
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 7a09ee459a6..0dae5c90394 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -2,11 +2,53 @@
class Release < ActiveRecord::Base
include CacheMarkdownField
+ include Gitlab::Utils::StrongMemoize
cache_markdown_field :description
belongs_to :project
+ # releases prior to 11.7 have no author
belongs_to :author, class_name: 'User'
+ has_many :links, class_name: 'Releases::Link'
+
+ accepts_nested_attributes_for :links, allow_destroy: true
+
validates :description, :project, :tag, presence: true
+
+ scope :sorted, -> { order(created_at: :desc) }
+
+ delegate :repository, to: :project
+
+ def commit
+ strong_memoize(:commit) do
+ repository.commit(actual_sha)
+ end
+ end
+
+ def tag_missing?
+ actual_tag.nil?
+ end
+
+ def assets_count
+ links.count + sources.count
+ end
+
+ def sources
+ strong_memoize(:sources) do
+ Releases::Source.all(project, tag)
+ end
+ end
+
+ private
+
+ def actual_sha
+ sha || actual_tag&.dereferenced_target
+ end
+
+ def actual_tag
+ strong_memoize(:actual_tag) do
+ repository.find_tag(tag)
+ end
+ end
end
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
new file mode 100644
index 00000000000..6c507c47752
--- /dev/null
+++ b/app/models/releases/link.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Releases
+ class Link < ActiveRecord::Base
+ self.table_name = 'release_links'
+
+ belongs_to :release
+
+ validates :url, presence: true, url: { protocols: %w(http https ftp) }, uniqueness: { scope: :release }
+ validates :name, presence: true, uniqueness: { scope: :release }
+
+ scope :sorted, -> { order(created_at: :desc) }
+
+ def internal?
+ url.start_with?(release.project.web_url)
+ end
+
+ def external?
+ !internal?
+ end
+ end
+end
diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb
new file mode 100644
index 00000000000..4d3d54457af
--- /dev/null
+++ b/app/models/releases/source.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Releases
+ class Source
+ include ActiveModel::Model
+
+ attr_accessor :project, :tag_name, :format
+
+ FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
+
+ class << self
+ def all(project, tag_name)
+ Releases::Source::FORMATS.map do |format|
+ Releases::Source.new(project: project,
+ tag_name: tag_name,
+ format: format)
+ end
+ end
+ end
+
+ def url
+ Gitlab::Routing
+ .url_helpers
+ .project_archive_url(project,
+ id: File.join(tag_name, archive_prefix),
+ format: format)
+ end
+
+ private
+
+ def archive_prefix
+ "#{project.path}-#{tag_name.tr('/', '-')}"
+ end
+ end
+end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 5a6895aefab..5eba7ddd75c 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -17,7 +17,7 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
+ validates :url, presence: true, public_url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
before_save :set_new_remote_name, if: :mirror_url_changed?
@@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base
timestamp = Time.now
remote_mirror.update!(
- last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
+ last_update_at: timestamp,
+ last_successful_update_at: timestamp,
+ last_error: nil,
+ error_notification_sent: false
)
end
@@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base
project.repository.add_remote(remote_name, remote_url)
end
+ def after_sent_notification
+ update_column(:error_notification_sent, true)
+ end
+
private
def store_credentials
@@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base
last_error: nil,
last_update_at: nil,
last_successful_update_at: nil,
- update_status: 'finished'
+ update_status: 'finished',
+ error_notification_sent: false
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 015a179f374..851175a61b7 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -25,6 +25,7 @@ class Repository
delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
+ AmbiguousRefError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
@@ -78,7 +79,7 @@ class Repository
end
def raw_repository
- return nil unless full_path
+ return unless full_path
@raw_repository ||= initialize_raw_repository
end
@@ -102,7 +103,7 @@ class Repository
end
def commit(ref = nil)
- return nil unless exists?
+ return unless exists?
return ref if ref.is_a?(::Commit)
find_commit(ref || root_ref)
@@ -181,6 +182,18 @@ class Repository
tags.find { |tag| tag.name == name }
end
+ def ambiguous_ref?(ref)
+ tag_exists?(ref) && branch_exists?(ref)
+ end
+
+ def expand_ref(ref)
+ if tag_exists?(ref)
+ Gitlab::Git::TAG_REF_PREFIX + ref
+ elsif branch_exists?(ref)
+ Gitlab::Git::BRANCH_REF_PREFIX + ref
+ end
+ end
+
def add_branch(user, branch_name, ref)
branch = raw_repository.add_branch(branch_name, user: user, target: ref)
@@ -275,13 +288,16 @@ class Repository
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind, number_commits_ahead =
- raw_repository.count_commits_between(
+ raw_repository.diverging_commit_count(
@root_ref_hash,
branch.dereferenced_target.sha,
- left_right: true,
max_count: MAX_DIVERGING_COUNT)
- { behind: number_commits_behind, ahead: number_commits_ahead }
+ if number_commits_behind + number_commits_ahead >= MAX_DIVERGING_COUNT
+ { distance: MAX_DIVERGING_COUNT }
+ else
+ { behind: number_commits_behind, ahead: number_commits_ahead }
+ end
end
end
@@ -512,6 +528,8 @@ class Repository
# items is an Array like: [[oid, path], [oid1, path1]]
def blobs_at(items)
+ return [] unless exists?
+
raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) }
end
@@ -599,7 +617,6 @@ class Repository
return unless readme
context = { project: project }
- context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled?
MarkupHelper.markup_unsafe(readme.name, readme.data, context)
end
@@ -837,6 +854,12 @@ class Repository
end
end
+ def merge_to_ref(user, source_sha, merge_request, target_ref, message)
+ branch = merge_request.target_branch
+
+ raw.merge_to_ref(user, source_sha, branch, target_ref, message)
+ end
+
def ff_merge(user, source, target_branch, merge_request: nil)
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
@@ -1016,12 +1039,12 @@ class Repository
remote_branch: merge_request.target_branch)
end
- def squash(user, merge_request)
+ def squash(user, merge_request, message)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
end_sha: merge_request.diff_head_sha,
author: merge_request.author,
- message: merge_request.title)
+ message: message)
end
def update_submodule(user, submodule, commit_sha, message:, branch:)
@@ -1059,19 +1082,11 @@ class Repository
end
def cache
- @cache ||= if is_wiki
- Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki')
- else
- Gitlab::RepositoryCache.new(self)
- end
+ @cache ||= Gitlab::RepositoryCache.new(self)
end
def request_store_cache
- @request_store_cache ||= if is_wiki
- Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki', backend: Gitlab::SafeRequestStore)
- else
- Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
- end
+ @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
def tags_sorted_by_committed_date
@@ -1098,6 +1113,9 @@ class Repository
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
+ Gitlab::Git::Repository.new(project.repository_storage,
+ disk_path + '.git',
+ Gitlab::GlRepository.gl_repository(project, is_wiki),
+ project.full_path)
end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index e65b3df0fb6..6caab24143b 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -48,7 +48,7 @@ class SentNotification < ActiveRecord::Base
end
def record_note(note, recipient_id, reply_key = self.reply_key, attrs = {})
- attrs[:in_reply_to_discussion_id] = note.discussion_id
+ attrs[:in_reply_to_discussion_id] = note.discussion_id if note.part_of_discussion?
record(note.noteable, recipient_id, reply_key, attrs)
end
@@ -99,29 +99,12 @@ class SentNotification < ActiveRecord::Base
private
def reply_params
- attrs = {
+ {
noteable_type: self.noteable_type,
noteable_id: self.noteable_id,
- commit_id: self.commit_id
+ commit_id: self.commit_id,
+ in_reply_to_discussion_id: self.in_reply_to_discussion_id
}
-
- if self.in_reply_to_discussion_id.present?
- attrs[:in_reply_to_discussion_id] = self.in_reply_to_discussion_id
- else
- # Remove in GitLab 10.0, when we will not support replying to SentNotifications
- # that don't have `in_reply_to_discussion_id` anymore.
- attrs.merge!(
- type: self.note_type,
-
- # LegacyDiffNote
- line_code: self.line_code,
-
- # DiffNote
- position: self.position.to_json
- )
- end
-
- attrs
end
def note_valid
diff --git a/app/models/service.rb b/app/models/service.rb
index 9dcb0aab0a3..da523bfa426 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -255,7 +255,6 @@ class Service < ActiveRecord::Base
external_wiki
flowdock
hangouts_chat
- hipchat
irker
jira
kubernetes
@@ -267,6 +266,7 @@ class Service < ActiveRecord::Base
prometheus
pushover
redmine
+ youtrack
slack_slash_commands
slack
teamcity
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 11856b55902..f23ddd64fe3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -50,11 +50,11 @@ class Snippet < ActiveRecord::Base
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
# Scopes
- scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
+ scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) }
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
- scope :fresh, -> { order("created_at DESC") }
+ scope :fresh, -> { order("created_at DESC") }
scope :inc_relations_for_view, -> { includes(author: :status) }
participant :author
@@ -175,6 +175,12 @@ class Snippet < ActiveRecord::Base
:visibility_level
end
+ def embeddable?
+ ability = project_id? ? :read_project_snippet : :read_personal_snippet
+
+ Ability.allowed?(nil, ability, self)
+ end
+
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index b6844dbe870..b6fb39ee81f 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -26,7 +26,8 @@ class SshHostKey
self.reactive_cache_lifetime = 10.minutes
def self.find_by(opts = {})
- return nil unless opts.key?(:id)
+ opts = HashWithIndifferentAccess.new(opts)
+ return unless opts.key?(:id)
project_id, url = opts[:id].split(':', 2)
project = Project.find_by(id: project_id)
@@ -52,6 +53,11 @@ class SshHostKey
@compare_host_keys = compare_host_keys
end
+ # Needed for reactive caching
+ def self.primary_key
+ :id
+ end
+
def id
[project.id, url].join(':')
end
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index 911fb7e9ce9..f5d0d6fab3b 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -31,7 +31,7 @@ module Storage
gitlab_shell.add_namespace(repository_storage, base_dir)
end
- def rename_repo
+ def rename_repo(old_full_path: nil, new_full_path: nil)
true
end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 9f6f19acb41..76ac5c13c18 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -29,18 +29,19 @@ module Storage
gitlab_shell.add_namespace(repository_storage, base_dir)
end
- def rename_repo
- new_full_path = project.build_full_path
+ def rename_repo(old_full_path: nil, new_full_path: nil)
+ old_full_path ||= project.full_path_was
+ new_full_path ||= project.build_full_path
- if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path)
+ if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions
begin
- gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
+ gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
return true
rescue => e
- Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}"
+ Rails.logger.error "Exception renaming #{old_full_path} -> #{new_full_path}: #{e}"
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
return false
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index c76b8e71507..09034646bff 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -5,8 +5,7 @@ class Suggestion < ApplicationRecord
validates :note, presence: true
validates :commit_id, presence: true, if: :applied?
- delegate :original_position, :position, :diff_file,
- :noteable, to: :note
+ delegate :original_position, :position, :noteable, to: :note
def project
noteable.source_project
@@ -16,6 +15,10 @@ class Suggestion < ApplicationRecord
noteable.source_branch
end
+ def file_path
+ position.file_path
+ end
+
# For now, suggestions only serve as a way to send patches that
# will change a single line (being able to apply multiple in the same place),
# which explains `from_line` and `to_line` being the same line.
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 7b64615f699..d9b86d941b6 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -4,6 +4,11 @@ class Todo < ActiveRecord::Base
include Sortable
include FromUnion
+ # Time to wait for todos being removed when not visible for user anymore.
+ # Prevents TODOs being removed by mistake, for example, removing access from a user
+ # and giving it back again.
+ WAIT_FOR_DELETE = 1.hour
+
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
diff --git a/app/models/user.rb b/app/models/user.rb
index 26fd2d903a1..778c9e631bd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,7 +2,7 @@
require 'carrierwave/orm/activerecord'
-class User < ActiveRecord::Base
+class User < ApplicationRecord
extend Gitlab::ConfigHelper
include Gitlab::ConfigHelper
@@ -145,7 +145,7 @@ class User < ActiveRecord::Base
has_many :issue_assignees
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
- has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
+ has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
@@ -228,6 +228,9 @@ class User < ActiveRecord::Base
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :notes_filter_for, to: :user_preference
delegate :set_notes_filter, to: :user_preference
+ delegate :first_day_of_week, :first_day_of_week=, to: :user_preference
+
+ accepts_nested_attributes_for :user_preference, update_only: true
state_machine :state, initial: :active do
event :block do
@@ -267,9 +270,12 @@ class User < ActiveRecord::Base
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
+ scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
+ scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) }
scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
+ scope :with_emails, -> { preload(:emails) }
# Limits the users to those that have TODOs, optionally in the given state.
#
@@ -337,6 +343,8 @@ class User < ActiveRecord::Base
case order_method.to_s
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
+ when 'last_activity_on_desc' then order_recent_last_activity
+ when 'last_activity_on_asc' then order_oldest_last_activity
else
order_by(order_method)
end
@@ -380,7 +388,7 @@ class User < ActiveRecord::Base
find_by(id: user_id)
end
- def filter(filter_name)
+ def filter_items(filter_name)
case filter_name
when 'admins'
admins
@@ -462,7 +470,7 @@ class User < ActiveRecord::Base
end
def by_login(login)
- return nil unless login
+ return unless login
if login.include?('@'.freeze)
unscoped.iwhere(email: login).take
@@ -754,8 +762,12 @@ class User < ActiveRecord::Base
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
- def authorizations_for_projects
- project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ def authorizations_for_projects(min_access_level: nil)
+ authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+
+ return authorizations unless min_access_level.present?
+
+ authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end
# Returns the projects this user has reporter (or greater) access to, limited
@@ -1156,6 +1168,10 @@ class User < ActiveRecord::Base
Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
end
+ def manageable_groups_with_routes
+ manageable_groups.eager_load(:route).order('routes.path')
+ end
+
def namespaces
namespace_ids = groups.pluck(:id)
namespace_ids.push(namespace.id)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index b1d6d461928..64daba81dcf 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -132,7 +132,7 @@ class WikiPage
# The GitLab Commit instance for this page.
def version
- return nil unless persisted?
+ return unless persisted?
@version ||= @page.version
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 0d0f1c28bad..72de04203a6 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -7,6 +7,10 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:admin) { @user&.admin? }
+ desc "User has access to all private groups & projects"
+ with_options scope: :user, score: 0
+ condition(:full_private_access) { @user&.full_private_access? }
+
with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? }
diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb
new file mode 100644
index 00000000000..4bf1e7bd3e1
--- /dev/null
+++ b/app/policies/board_policy.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BoardPolicy < BasePolicy
+ delegate { @subject.parent }
+
+ condition(:is_group_board) { @subject.group_board? }
+ condition(:is_project_board) { @subject.project_board? }
+
+ rule { is_project_board & can?(:read_project) }.enable :read_parent
+
+ rule { is_group_board & can?(:read_group) }.policy do
+ enable :read_parent
+ enable :read_milestone
+ enable :read_issue
+ end
+end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index e42d78f47c5..2c90b8a73cd 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -10,6 +10,15 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:external_pipeline, scope: :subject, score: 0) do
+ @subject.external?
+ end
+
+ # Disallow users without permissions from accessing internal pipelines
+ rule { ~can?(:read_build) & ~external_pipeline }.policy do
+ prevent :read_pipeline
+ end
+
rule { protected_ref }.prevent :update_pipeline
rule { can?(:public_access) & branch_allows_collaboration }.policy do
diff --git a/app/policies/concerns/clusterable_actions.rb b/app/policies/concerns/clusterable_actions.rb
new file mode 100644
index 00000000000..08ddd742ea9
--- /dev/null
+++ b/app/policies/concerns/clusterable_actions.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ClusterableActions
+ private
+
+ # Overridden on EE module
+ def multiple_clusters_available?
+ false
+ end
+
+ def clusterable_has_clusters?
+ !subject.clusters.empty?
+ end
+end
diff --git a/app/policies/container_repository_policy.rb b/app/policies/container_repository_policy.rb
new file mode 100644
index 00000000000..6781c845142
--- /dev/null
+++ b/app/policies/container_repository_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ContainerRepositoryPolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 16c58730878..d412a591fdc 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -68,6 +68,10 @@ class GlobalPolicy < BasePolicy
enable :read_users_list
end
+ rule { ~anonymous }.policy do
+ enable :read_instance_metadata
+ end
+
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index f07bb188265..e74e5f008d7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GroupPolicy < BasePolicy
+ include ClusterableActions
+
desc "Group is public"
with_options scope: :subject, score: 0
condition(:public_group) { @subject.public? }
@@ -27,6 +29,9 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end
+ condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
+ condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
@@ -45,10 +50,10 @@ class GroupPolicy < BasePolicy
enable :read_label
end
- rule { admin } .enable :read_group
+ rule { admin }.enable :read_group
rule { has_projects }.policy do
- enable :read_group
+ enable :read_list
enable :read_label
end
@@ -67,6 +72,7 @@ class GroupPolicy < BasePolicy
enable :admin_pipeline
enable :admin_build
enable :read_cluster
+ enable :add_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
@@ -106,6 +112,8 @@ class GroupPolicy < BasePolicy
rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
+ rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+
def access_level
return GroupMember::NO_ACCESS if @user.nil?
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 6d8b575102e..ecb2797d1d9 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -11,7 +11,7 @@ class IssuablePolicy < BasePolicy
@user && @subject.assignee_or_author?(@user)
end
- rule { assignee_or_author }.policy do
+ rule { can?(:guest_access) & assignee_or_author }.policy do
enable :read_issue
enable :update_issue
enable :reopen_issue
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a0706eaa46c..dd8c5d49cf4 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -18,6 +18,7 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
+ prevent :create_note
end
rule { locked }.policy do
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index f22843b6463..8d23e3abed3 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -18,6 +18,7 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
+ prevent :award_emoji
end
rule { is_author }.policy do
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 777f933cdcd..2b5cca76c20 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -28,5 +28,10 @@ class PersonalSnippetPolicy < BasePolicy
rule { anonymous }.prevent :comment_personal_snippet
- rule { can?(:comment_personal_snippet) }.enable :award_emoji
+ rule { can?(:comment_personal_snippet) }.policy do
+ enable :create_note
+ enable :award_emoji
+ end
+
+ rule { full_private_access }.enable :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 1c082945299..cf257ed47c8 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,6 +2,7 @@
class ProjectPolicy < BasePolicy
extend ClassMethods
+ include ClusterableActions
READONLY_FEATURES_WHEN_ARCHIVED = %i[
issue
@@ -22,6 +23,7 @@ class ProjectPolicy < BasePolicy
container_image
pages
cluster
+ release
].freeze
desc "User is a project owner"
@@ -103,6 +105,13 @@ class ProjectPolicy < BasePolicy
@subject.feature_available?(:merge_requests, @user)
end
+ condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
+ condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+
+ condition(:internal_builds_disabled) do
+ !@subject.builds_enabled?
+ end
+
features = %w[
merge_requests
issues
@@ -143,7 +152,6 @@ class ProjectPolicy < BasePolicy
enable :remove_fork_project
enable :destroy_merge_request
enable :destroy_issue
- enable :remove_pages
enable :set_issue_iid
enable :set_issue_created_at
@@ -169,6 +177,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :award_emoji
enable :read_pages_content
+ enable :read_release
end
# These abilities are not allowed to admins that are not members of the project,
@@ -190,10 +199,10 @@ class ProjectPolicy < BasePolicy
enable :read_build
enable :read_container_image
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_merge_request
+ enable :read_sentry_issue
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
@@ -216,6 +225,8 @@ class ProjectPolicy < BasePolicy
rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access
+ rule { can?(:developer_access) & can?(:create_issue) }.enable :import_issues
+
rule { can?(:developer_access) }.policy do
enable :admin_merge_request
enable :admin_milestone
@@ -226,6 +237,7 @@ class ProjectPolicy < BasePolicy
enable :update_build
enable :create_pipeline
enable :update_pipeline
+ enable :read_pipeline_schedule
enable :create_pipeline_schedule
enable :create_merge_request_from
enable :create_wiki
@@ -235,6 +247,8 @@ class ProjectPolicy < BasePolicy
enable :update_container_image
enable :create_environment
enable :create_deployment
+ enable :create_release
+ enable :update_release
end
rule { can?(:maintainer_access) }.policy do
@@ -256,11 +270,16 @@ class ProjectPolicy < BasePolicy
enable :admin_pages
enable :read_pages
enable :update_pages
+ enable :remove_pages
enable :read_cluster
+ enable :add_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
enable :create_environment_terminal
+ enable :destroy_release
+ enable :destroy_artifacts
+ enable :daily_statistics
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
@@ -282,6 +301,8 @@ class ProjectPolicy < BasePolicy
rule { issues_disabled }.policy do
prevent(*create_read_update_admin_destroy(:issue))
+ prevent(*create_read_update_admin_destroy(:board))
+ prevent(*create_read_update_admin_destroy(:list))
end
rule { merge_requests_disabled | repository_disabled }.policy do
@@ -301,13 +322,12 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:project_snippet))
end
- rule { wiki_disabled & ~has_external_wiki }.policy do
+ rule { wiki_disabled }.policy do
prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
- prevent(*create_update_admin_destroy(:pipeline))
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
@@ -315,11 +335,23 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:deployment))
end
+ # There's two separate cases when builds_disabled is true:
+ # 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
+ # - We do not prevent the user from accessing Pipelines to allow him to access external CI
+ # 2. When the user is not allowed to access CI - builds_disabled && ~internal_builds_disabled
+ # - We prevent the user from accessing Pipelines
+ rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
+ prevent(*create_read_update_admin_destroy(:pipeline))
+ prevent(*create_read_update_admin_destroy(:commit_status))
+ end
+
rule { repository_disabled }.policy do
prevent :push_code
prevent :download_code
prevent :fork_project
prevent :read_commit_status
+ prevent :read_pipeline
+ prevent(*create_read_update_admin_destroy(:release))
end
rule { container_registry_disabled }.policy do
@@ -345,10 +377,10 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_commit_status
enable :read_container_image
enable :download_code
+ enable :read_release
enable :download_wiki_code
enable :read_cycle_analytics
enable :read_pages_content
@@ -363,7 +395,6 @@ class ProjectPolicy < BasePolicy
rule { public_builds & can?(:guest_access) }.policy do
enable :read_pipeline
- enable :read_pipeline_schedule
end
# These rules are included to allow maintainers of projects to push to certain
@@ -378,9 +409,11 @@ class ProjectPolicy < BasePolicy
end.enable :read_issue_iid
rule do
- (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
+ (~guest & can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
end.enable :read_merge_request_iid
+ rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+
private
def team_member?
@@ -433,7 +466,7 @@ class ProjectPolicy < BasePolicy
when ProjectFeature::DISABLED
false
when ProjectFeature::PRIVATE
- guest? || admin?
+ admin? || team_access_level >= ProjectFeature.required_minimum_access_level(feature)
else
true
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 288bf070cfc..e5e005cee6d 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -5,13 +5,12 @@ class ProjectSnippetPolicy < BasePolicy
desc "Snippet is public"
condition(:public_snippet, scope: :subject) { @subject.public? }
+ condition(:internal_snippet, scope: :subject) { @subject.internal? }
condition(:private_snippet, scope: :subject) { @subject.private? }
condition(:public_project, scope: :subject) { @subject.project.public? }
condition(:is_author) { @user && @subject.author == @user }
- condition(:internal, scope: :subject) { @subject.internal? }
-
# We have to check both project feature visibility and a snippet visibility and take the stricter one
# This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573
rule { ~can?(:read_project) }.policy do
@@ -26,13 +25,13 @@ class ProjectSnippetPolicy < BasePolicy
# is used to hide/show various snippet-related controls, so we can't just move
# all of the handling here.
rule do
- all?(private_snippet | (internal & external_user),
+ all?(private_snippet | (internal_snippet & external_user),
~project.guest,
- ~admin,
- ~is_author)
+ ~is_author,
+ ~full_private_access)
end.prevent :read_project_snippet
- rule { internal & ~is_author & ~admin }.policy do
+ rule { internal_snippet & ~is_author & ~admin }.policy do
prevent :update_project_snippet
prevent :admin_project_snippet
end
@@ -44,4 +43,6 @@ class ProjectSnippetPolicy < BasePolicy
enable :update_project_snippet
enable :admin_project_snippet
end
+
+ rule { ~can?(:read_project_snippet) }.prevent :create_note
end
diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb
new file mode 100644
index 00000000000..d7f9e5d7445
--- /dev/null
+++ b/app/policies/release_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ReleasePolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 300f85e1e9d..29656b17183 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -2,6 +2,11 @@
module Ci
class BuildRunnerPresenter < SimpleDelegator
+ include Gitlab::Utils::StrongMemoize
+
+ RUNNER_REMOTE_TAG_PREFIX = 'refs/tags/'.freeze
+ RUNNER_REMOTE_BRANCH_PREFIX = 'refs/remotes/origin/'.freeze
+
def artifacts
return unless options[:artifacts]
@@ -11,6 +16,35 @@ module Ci
list.flatten.compact
end
+ def ref_type
+ if tag
+ 'tag'
+ else
+ 'branch'
+ end
+ end
+
+ def git_depth
+ strong_memoize(:git_depth) do
+ git_depth = variables&.find { |variable| variable[:key] == 'GIT_DEPTH' }&.dig(:value)
+ git_depth.to_i
+ end
+ end
+
+ def refspecs
+ specs = []
+
+ if git_depth > 0
+ specs << refspec_for_branch(ref) if branch? || merge_request_event?
+ specs << refspec_for_tag(ref) if tag?
+ else
+ specs << refspec_for_branch
+ specs << refspec_for_tag
+ end
+
+ specs
+ end
+
private
def create_archive(artifacts)
@@ -41,5 +75,13 @@ module Ci
}
end
end
+
+ def refspec_for_branch(ref = '*')
+ "+#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_BRANCH_PREFIX}#{ref}"
+ end
+
+ def refspec_for_tag(ref = '*')
+ "+#{Gitlab::Git::TAG_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_TAG_PREFIX}#{ref}"
+ end
end
end
diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb
new file mode 100644
index 00000000000..605c8f328a4
--- /dev/null
+++ b/app/presenters/ci/trigger_presenter.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class TriggerPresenter < Gitlab::View::Presenter::Delegated
+ presents :trigger
+
+ def has_token_exposed?
+ can?(current_user, :admin_trigger, trigger)
+ end
+
+ def token
+ if has_token_exposed?
+ trigger.token
+ else
+ trigger.short_token
+ end
+ end
+ end
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 9cc137aa3bd..d94d9118eee 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -12,6 +12,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
.fabricate!
end
+ def can_add_cluster?
+ can?(current_user, :add_cluster, clusterable)
+ end
+
def can_create_cluster?
can?(current_user, :create_cluster, clusterable)
end
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
new file mode 100644
index 00000000000..05adbe1d4f5
--- /dev/null
+++ b/app/presenters/commit_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CommitPresenter < Gitlab::View::Presenter::Simple
+ presents :commit
+
+ def status_for(ref)
+ can?(current_user, :read_commit_status, commit.project) && commit.status(ref)
+ end
+
+ def any_pipelines?
+ can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any?
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 1db6c9eff36..af164858408 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -98,6 +98,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
+ def target_branch_path
+ if target_branch_exists?
+ project_branch_path(project, target_branch)
+ end
+ end
+
def source_branch_path
if source_branch_exists?
project_branch_path(source_project, source_branch)
@@ -170,6 +176,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
+ def can_read_pipeline?
+ pipeline && can?(current_user, :read_pipeline, pipeline)
+ end
+
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
@@ -189,6 +199,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
merge_request.subscribed?(current_user, merge_request.target_project)
end
+ def conflicts_docs_path
+ help_page_path('user/project/merge_requests/resolve_conflicts.md')
+ end
+
private
def cached_can_be_reverted?
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 9bd64ea217e..000b7c433a2 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -13,7 +13,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
presents :project
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon)
- MAX_TAGS_TO_SHOW = 3
+ MAX_TOPICS_TO_SHOW = 3
def statistic_icon(icon_name = 'plus-square-o')
sprite_icon(icon_name, size: 16, css_class: 'icon append-right-4')
@@ -256,7 +256,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
elsif repository.contribution_guide.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CONTRIBUTING'),
- contribution_guide_path)
+ contribution_guide_path,
+ 'default')
end
end
@@ -310,20 +311,24 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
end
- def tags_to_show
- project.tag_list.take(MAX_TAGS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord
+ def topics_to_show
+ project.tag_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord
end
- def count_of_extra_tags_not_shown
- if project.tag_list.count > MAX_TAGS_TO_SHOW
- project.tag_list.count - MAX_TAGS_TO_SHOW
+ def topics_not_shown
+ project.tag_list - topics_to_show
+ end
+
+ def count_of_extra_topics_not_shown
+ if project.tag_list.count > MAX_TOPICS_TO_SHOW
+ project.tag_list.count - MAX_TOPICS_TO_SHOW
else
0
end
end
- def has_extra_tags?
- count_of_extra_tags_not_shown > 0
+ def has_extra_topics?
+ count_of_extra_topics_not_shown > 0
end
private
diff --git a/app/serializers/acts_as_taggable_on/tag_entity.rb b/app/serializers/acts_as_taggable_on/tag_entity.rb
new file mode 100644
index 00000000000..d4e4b69f8fa
--- /dev/null
+++ b/app/serializers/acts_as_taggable_on/tag_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class ActsAsTaggableOn::TagEntity < Grape::Entity
+ expose :id
+ expose :name
+end
diff --git a/app/serializers/acts_as_taggable_on/tag_serializer.rb b/app/serializers/acts_as_taggable_on/tag_serializer.rb
new file mode 100644
index 00000000000..87f53606aa1
--- /dev/null
+++ b/app/serializers/acts_as_taggable_on/tag_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActsAsTaggableOn::TagSerializer < BaseSerializer
+ entity ActsAsTaggableOn::TagEntity
+end
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
index 7b65bd22f54..4744a7c1cc8 100644
--- a/app/serializers/base_serializer.rb
+++ b/app/serializers/base_serializer.rb
@@ -16,11 +16,11 @@ class BaseSerializer
.as_json
end
- def self.entity(entity_class)
- @entity_class ||= entity_class
- end
+ class << self
+ attr_reader :entity_class
- def self.entity_class
- @entity_class
+ def entity(entity_class)
+ @entity_class ||= entity_class
+ end
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 7b1a0be75ca..02df1480828 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -4,7 +4,9 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
+ expose :version
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
+ expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
end
diff --git a/app/serializers/concerns/user_status_tooltip.rb b/app/serializers/concerns/user_status_tooltip.rb
index aa6e67e3351..633b117d392 100644
--- a/app/serializers/concerns/user_status_tooltip.rb
+++ b/app/serializers/concerns/user_status_tooltip.rb
@@ -11,7 +11,7 @@ module UserStatusTooltip
expose :user_status_if_loaded, as: :status_tooltip_html
def user_status_if_loaded
- return nil unless object.association(:status).loaded?
+ return unless object.association(:status).loaded?
user_status(object)
end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
index 59bf35f5aff..cc746698a05 100644
--- a/app/serializers/container_repository_entity.rb
+++ b/app/serializers/container_repository_entity.rb
@@ -3,7 +3,7 @@
class ContainerRepositoryEntity < Grape::Entity
include RequestAwareEntity
- expose :id, :path, :location
+ expose :id, :name, :path, :location, :created_at
expose :tags_path do |repository|
project_registry_repository_tags_path(project, repository, format: :json)
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
index 637294877f8..361c073e22e 100644
--- a/app/serializers/container_tag_entity.rb
+++ b/app/serializers/container_tag_entity.rb
@@ -3,7 +3,7 @@
class ContainerTagEntity < Grape::Entity
include RequestAwareEntity
- expose :name, :location, :revision, :short_revision, :total_size, :created_at
+ expose :name, :path, :location, :digest, :revision, :short_revision, :total_size, :created_at
expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
project_registry_repository_tag_path(project, tag.repository, tag.name)
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index aa1d9e6292c..34ae06278c8 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -24,6 +24,12 @@ class DeploymentEntity < Grape::Entity
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: JobEntity
- expose :manual_actions, using: JobEntity
- expose :scheduled_actions, using: JobEntity
+ expose :manual_actions, using: JobEntity, if: -> (*) { can_create_deployment? }
+ expose :scheduled_actions, using: JobEntity, if: -> (*) { can_create_deployment? }
+
+ private
+
+ def can_create_deployment?
+ can?(request.current_user, :create_deployment, request.project)
+ end
end
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index 06a8db78476..d8630165e49 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -27,9 +27,13 @@ class DiffFileBaseEntity < Grape::Entity
next unless merge_request.source_project
- project_edit_blob_path(merge_request.source_project,
- tree_join(merge_request.source_branch, diff_file.new_path),
- options)
+ if Feature.enabled?(:web_ide_default)
+ ide_edit_path(merge_request.source_project, merge_request.source_branch, diff_file.new_path)
+ else
+ project_edit_blob_path(merge_request.source_project,
+ tree_join(merge_request.source_branch, diff_file.new_path),
+ options)
+ end
end
expose :old_path_html do |diff_file|
@@ -72,17 +76,20 @@ class DiffFileBaseEntity < Grape::Entity
expose :old_path
expose :new_path
expose :new_file?, as: :new_file
- expose :collapsed?, as: :collapsed
- expose :text?, as: :text
+ expose :renamed_file?, as: :renamed_file
+ expose :deleted_file?, as: :deleted_file
+
expose :diff_refs
+
expose :stored_externally?, as: :stored_externally
expose :external_storage
- expose :renamed_file?, as: :renamed_file
- expose :deleted_file?, as: :deleted_file
+
expose :mode_changed?, as: :mode_changed
expose :a_mode
expose :b_mode
+ expose :viewer, using: DiffViewerEntity
+
private
def memoized_submodule_links(diff_file)
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index b0aaec3326d..01ee7af37ed 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -4,12 +4,10 @@ class DiffFileEntity < DiffFileBaseEntity
include CommitsHelper
include IconsHelper
- expose :too_large?, as: :too_large
- expose :empty?, as: :empty
expose :added_lines
expose :removed_lines
- expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file|
+ expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.viewer.collapsed? && options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
project = merge_request.target_project
@@ -36,10 +34,6 @@ class DiffFileEntity < DiffFileBaseEntity
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end
- expose :viewer, using: DiffViewerEntity do |diff_file|
- diff_file.rich_viewer || diff_file.simple_viewer
- end
-
expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb
index 27fba03cb3f..45faca6cb2f 100644
--- a/app/serializers/diff_viewer_entity.rb
+++ b/app/serializers/diff_viewer_entity.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class DiffViewerEntity < Grape::Entity
- # Partial name refers directly to a Rails feature, let's avoid
- # using this on the frontend.
expose :partial_name, as: :name
+ expose :render_error, as: :error
+ expose :render_error_message, as: :error_message
+ expose :collapsed?, as: :collapsed
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 4a7d13915dd..76248e6470e 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -8,6 +8,7 @@ class EnvironmentEntity < Grape::Entity
expose :state
expose :external_url
expose :environment_type
+ expose :name_without_type
expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action
diff --git a/app/serializers/error_tracking/error_entity.rb b/app/serializers/error_tracking/error_entity.rb
new file mode 100644
index 00000000000..91388e7c3ad
--- /dev/null
+++ b/app/serializers/error_tracking/error_entity.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorEntity < Grape::Entity
+ expose :id, :title, :type, :user_count, :count,
+ :first_seen, :last_seen, :message, :culprit,
+ :external_url, :project_id, :project_name, :project_slug,
+ :short_id, :status, :frequency
+ end
+end
diff --git a/app/serializers/error_tracking/error_serializer.rb b/app/serializers/error_tracking/error_serializer.rb
new file mode 100644
index 00000000000..ff9a645eb16
--- /dev/null
+++ b/app/serializers/error_tracking/error_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorSerializer < BaseSerializer
+ entity ErrorEntity
+ end
+end
diff --git a/app/serializers/error_tracking/project_entity.rb b/app/serializers/error_tracking/project_entity.rb
new file mode 100644
index 00000000000..405d87ca0d0
--- /dev/null
+++ b/app/serializers/error_tracking/project_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectEntity < Grape::Entity
+ expose(*Gitlab::ErrorTracking::Project::ACCESSORS)
+ end
+end
diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb
new file mode 100644
index 00000000000..68724088fff
--- /dev/null
+++ b/app/serializers/error_tracking/project_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectSerializer < BaseSerializer
+ entity ErrorTracking::ProjectEntity
+ end
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 084627f9dbe..178e72f4f0a 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -11,4 +11,5 @@ class MergeRequestBasicEntity < Grape::Entity
expose :labels, using: LabelEntity
expose :assignee, using: API::Entities::UserBasic
expose :task_status, :task_status_short
+ expose :lock_version, :lock_version
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
index 433bfe60474..7e3053e5881 100644
--- a/app/serializers/merge_request_diff_entity.rb
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -24,6 +24,14 @@ class MergeRequestDiffEntity < Grape::Entity
short_sha(merge_request_diff.head_commit_sha)
end
+ expose :base_version_path do |merge_request_diff|
+ project = merge_request.target_project
+
+ next unless project
+
+ merge_request_version_path(project, merge_request, merge_request_diff)
+ end
+
expose :version_path do |merge_request_diff|
start_sha = options[:start_sha]
project = merge_request.target_project
diff --git a/app/serializers/merge_request_for_pipeline_entity.rb b/app/serializers/merge_request_for_pipeline_entity.rb
new file mode 100644
index 00000000000..7779ddfd65a
--- /dev/null
+++ b/app/serializers/merge_request_for_pipeline_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class MergeRequestForPipelineEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :iid
+
+ expose :path do |merge_request|
+ project_merge_request_path(merge_request.project, merge_request)
+ end
+
+ expose :title
+ expose :source_branch
+ expose :source_branch_path
+ expose :target_branch
+ expose :target_branch_path
+end
diff --git a/app/serializers/merge_request_widget_commit_entity.rb b/app/serializers/merge_request_widget_commit_entity.rb
new file mode 100644
index 00000000000..50a5c44a6ad
--- /dev/null
+++ b/app/serializers/merge_request_widget_commit_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestWidgetCommitEntity < Grape::Entity
+ expose :safe_message, as: :message
+ expose :short_id
+ expose :title
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 9731b52f1ad..2142ceb6122 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -11,6 +11,9 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_user_id
expose :merge_when_pipeline_succeeds
expose :source_branch
+ expose :source_branch_protected do |merge_request|
+ merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
+ end
expose :source_project_id
expose :source_project_full_path do |merge_request|
merge_request.source_project&.full_path
@@ -53,10 +56,23 @@ class MergeRequestWidgetEntity < IssuableEntity
merge_request.diff_head_sha.presence
end
- expose :merge_commit_message
- expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
+
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
+ expose :default_squash_commit_message
+ expose :default_merge_commit_message
+
+ expose :default_merge_commit_message_with_description do |merge_request|
+ merge_request.default_merge_commit_message(include_description: true)
+ end
+
+ expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
+ merge_request.commits.without_merge_commits
+ end
+
+ expose :commits_count
+
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
@@ -74,7 +90,6 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :branch_missing?, as: :branch_missing
- expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :mergeable?, as: :mergeable
@@ -202,10 +217,6 @@ class MergeRequestWidgetEntity < IssuableEntity
ci_environments_status_project_merge_request_path(merge_request.project, merge_request)
end
- expose :merge_commit_message_with_description do |merge_request|
- merge_request.merge_commit_message(include_description: true)
- end
-
expose :diverged_commits_count do |merge_request|
if merge_request.open? && merge_request.diverged_from_target_branch?
merge_request.diverged_commits_count
@@ -240,6 +251,10 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :supports_suggestion?, as: :can_receive_suggestion
+ expose :conflicts_docs_path do |merge_request|
+ presenter(merge_request).conflicts_docs_path
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/namespace_basic_entity.rb b/app/serializers/namespace_basic_entity.rb
new file mode 100644
index 00000000000..8bcbb2bca60
--- /dev/null
+++ b/app/serializers/namespace_basic_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class NamespaceBasicEntity < Grape::Entity
+ expose :id
+ expose :full_path
+end
diff --git a/app/serializers/namespace_serializer.rb b/app/serializers/namespace_serializer.rb
new file mode 100644
index 00000000000..bf3f154b558
--- /dev/null
+++ b/app/serializers/namespace_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class NamespaceSerializer < BaseSerializer
+ entity NamespaceBasicEntity
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index c9669e59199..5ac1e590d39 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -23,11 +23,12 @@ class PipelineEntity < Grape::Entity
expose :latest?, as: :latest
expose :stuck?, as: :stuck
expose :auto_devops_source?, as: :auto_devops
- expose :merge_request?, as: :merge_request
+ expose :merge_request_event?, as: :merge_request
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
+ expose :detached_merge_request_pipeline?, as: :detached
end
expose :details do
@@ -36,6 +37,10 @@ class PipelineEntity < Grape::Entity
expose :finished_at
end
+ expose :merge_request, if: -> (*) { has_presentable_merge_request? }, with: MergeRequestForPipelineEntity do |pipeline|
+ pipeline.merge_request.present(current_user: request.current_user)
+ end
+
expose :ref do
expose :name do |pipeline|
pipeline.ref
@@ -49,7 +54,7 @@ class PipelineEntity < Grape::Entity
expose :tag?, as: :tag
expose :branch?, as: :branch
- expose :merge_request?, as: :merge_request
+ expose :merge_request_event?, as: :merge_request
end
expose :commit, using: CommitEntity
@@ -59,7 +64,7 @@ class PipelineEntity < Grape::Entity
pipeline.present.failure_reason
end
- expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
+ expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_project_pipeline_path(pipeline.project, pipeline)
end
@@ -81,6 +86,11 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable?
end
+ def has_presentable_merge_request?
+ pipeline.triggered_by_merge_request? &&
+ can?(request.current_user, :read_merge_request, pipeline.merge_request)
+ end
+
def detailed_status
pipeline.detailed_status(request.current_user)
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 7451433a841..dbbeca9431d 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -15,6 +15,7 @@ class PipelineSerializer < BaseSerializer
:manual_actions,
:scheduled_actions,
:artifacts,
+ :merge_request,
{
pending_builds: :project,
project: [:route, { namespace: :route }],
diff --git a/app/serializers/project_import_entity.rb b/app/serializers/project_import_entity.rb
new file mode 100644
index 00000000000..9b51af685e7
--- /dev/null
+++ b/app/serializers/project_import_entity.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectImportEntity < ProjectEntity
+ include ImportHelper
+
+ expose :import_source
+ expose :import_status
+ expose :human_import_status_name
+
+ expose :provider_link do |project, options|
+ provider_project_link_url(options[:provider_url], project[:import_source])
+ end
+end
diff --git a/app/serializers/project_serializer.rb b/app/serializers/project_serializer.rb
index 23b96c2fc9e..52ac2fa0e09 100644
--- a/app/serializers/project_serializer.rb
+++ b/app/serializers/project_serializer.rb
@@ -1,5 +1,15 @@
# frozen_string_literal: true
class ProjectSerializer < BaseSerializer
- entity ProjectEntity
+ def represent(project, opts = {})
+ entity =
+ case opts[:serializer]
+ when :import
+ ProjectImportEntity
+ else
+ ProjectEntity
+ end
+
+ super(project, opts, entity)
+ end
end
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
index 4f1f62d145b..c98dc1a1c4a 100644
--- a/app/serializers/projects/serverless/service_entity.rb
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -13,6 +13,25 @@ module Projects
service.dig('metadata', 'namespace')
end
+ expose :environment_scope do |service|
+ service.dig('environment_scope')
+ end
+
+ expose :cluster_id do |service|
+ service.dig('cluster_id')
+ end
+
+ expose :detail_url do |service|
+ project_serverless_path(
+ request.project,
+ service.dig('environment_scope'),
+ service.dig('metadata', 'name'))
+ end
+
+ expose :podcount do |service|
+ service.dig('podcount')
+ end
+
expose :created_at do |service|
service.dig('metadata', 'creationTimestamp')
end
@@ -22,11 +41,24 @@ module Projects
end
expose :description do |service|
- service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description')
+ service.dig(
+ 'spec',
+ 'runLatest',
+ 'configuration',
+ 'revisionTemplate',
+ 'metadata',
+ 'annotations',
+ 'Description')
end
expose :image do |service|
- service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name')
+ service.dig(
+ 'spec',
+ 'runLatest',
+ 'configuration',
+ 'build',
+ 'template',
+ 'name')
end
end
end
diff --git a/app/serializers/provider_repo_entity.rb b/app/serializers/provider_repo_entity.rb
new file mode 100644
index 00000000000..d70aaa91324
--- /dev/null
+++ b/app/serializers/provider_repo_entity.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class ProviderRepoEntity < Grape::Entity
+ include ImportHelper
+
+ expose :id
+ expose :full_name
+ expose :owner_name do |provider_repo, options|
+ owner_name(provider_repo, options[:provider])
+ end
+
+ expose :sanitized_name do |provider_repo|
+ sanitize_project_name(provider_repo[:name])
+ end
+
+ expose :provider_link do |provider_repo, options|
+ provider_project_link_url(options[:provider_url], provider_repo[:full_name])
+ end
+
+ private
+
+ def owner_name(provider_repo, provider)
+ provider_repo.dig(:owner, :login) if provider == :github
+ end
+end
diff --git a/app/serializers/provider_repo_serializer.rb b/app/serializers/provider_repo_serializer.rb
new file mode 100644
index 00000000000..8a73f6fe6df
--- /dev/null
+++ b/app/serializers/provider_repo_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ProviderRepoSerializer < BaseSerializer
+ entity ProviderRepoEntity
+end
diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb
deleted file mode 100644
index 9b7dc80e1d9..00000000000
--- a/app/serializers/tree_entity.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class TreeEntity < Grape::Entity
- include RequestAwareEntity
-
- expose :id, :path, :name, :mode
-
- expose :icon do |tree|
- IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
- end
-
- expose :url do |tree|
- project_tree_path(request.project, File.join(request.ref, tree.path))
- end
-end
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
deleted file mode 100644
index f1cfcd943d8..00000000000
--- a/app/serializers/tree_root_entity.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
-class TreeRootEntity < Grape::Entity
- include RequestAwareEntity
-
- expose :path
-
- expose :trees, using: TreeEntity
- expose :blobs, using: BlobEntity
- expose :submodules, using: SubmoduleEntity
-
- expose :parent_tree_url do |tree|
- path = tree.path.sub(%r{\A/}, '')
- next unless path.present?
-
- path_segments = path.split('/')
- path_segments.pop
- parent_tree_path = path_segments.join('/')
-
- project_tree_path(request.project, File.join(request.ref, parent_tree_path))
- end
-
- expose :last_commit_path do |tree|
- logs_file_project_ref_path(request.project, request.ref, tree.path)
- end
-end
diff --git a/app/serializers/tree_serializer.rb b/app/serializers/tree_serializer.rb
deleted file mode 100644
index 536b8ab1ae2..00000000000
--- a/app/serializers/tree_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class TreeSerializer < BaseSerializer
- entity TreeRootEntity
-end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 2e4643ed668..9146eb96533 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -38,7 +38,7 @@ module ApplicationSettings
def performance_bar_allowed_group_id
performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
group_full_path = params.delete(:performance_bar_allowed_group_path)
- return nil unless Gitlab::Utils.to_boolean(performance_bar_enabled)
+ return unless Gitlab::Utils.to_boolean(performance_bar_enabled)
Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
end
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index b6c30da4d3a..dff0d9696f8 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -2,16 +2,16 @@
module Applications
class CreateService
- # rubocop: disable CodeReuse/ActiveRecord
+ attr_reader :current_user, :params
+
def initialize(current_user, params)
@current_user = current_user
- @params = params.except(:ip_address)
+ @params = params.except(:ip_address) # rubocop: disable CodeReuse/ActiveRecord
end
- # rubocop: enable CodeReuse/ActiveRecord
# EE would override and use `request` arg
def execute(request)
- Doorkeeper::Application.create(@params)
+ Doorkeeper::Application.create(params)
end
end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index f764536e762..e95ba09c006 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -160,7 +160,8 @@ module Auth
##
# We still support legacy pipeline triggers which do not have associated
# actor. New permissions model and new triggers are always associated with
- # an actor, so this should be improved in 10.0 version of GitLab.
+ # an actor. So this should be improved once
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/37452 is resolved.
#
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 43a26f4264e..834baeb9643 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -50,7 +50,7 @@ module Boards
if move_between_ids
attrs[:move_between_ids] = move_between_ids
- attrs[:board_group_id] = board.group&.id
+ attrs[:board_group_id] = board.group&.id
end
attrs
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index 609c430caed..e20805d0405 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -20,7 +20,7 @@ module Boards
# rubocop: disable CodeReuse/ActiveRecord
def decrement_higher_lists(list)
- board.lists.movable.where('position > ?', list.position)
+ board.lists.movable.where('position > ?', list.position)
.update_all('position = position - 1')
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb
index 9e4c77a6317..d13e25b4f12 100644
--- a/app/services/boards/visits/latest_service.rb
+++ b/app/services/boards/visits/latest_service.rb
@@ -4,13 +4,15 @@ module Boards
module Visits
class LatestService < Boards::BaseService
def execute
- return nil unless current_user
+ return unless current_user
- if parent.is_a?(Group)
- BoardGroupRecentVisit.latest(current_user, parent)
- else
- BoardProjectRecentVisit.latest(current_user, parent)
- end
+ recent_visit_model.latest(current_user, parent, count: params[:count])
+ end
+
+ private
+
+ def recent_visit_model
+ parent.is_a?(Group) ? BoardGroupRecentVisit : BoardProjectRecentVisit
end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 19b5552887f..8973c5ffc9e 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -7,14 +7,17 @@ module Ci
CreateError = Class.new(StandardError)
SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
+ Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate,
- Gitlab::Ci::Pipeline::Chain::Create].freeze
+ Gitlab::Ci::Pipeline::Chain::Create,
+ Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, &block)
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -22,7 +25,9 @@ module Ci
origin_ref: params[:ref],
checkout_sha: params[:checkout_sha],
after_sha: params[:after],
- before_sha: params[:before],
+ before_sha: params[:before], # The base SHA of the source branch (i.e merge_request.diff_base_sha).
+ source_sha: params[:source_sha], # The HEAD SHA of the source branch (i.e merge_request.diff_head_sha).
+ target_sha: params[:target_sha], # The HEAD SHA of the target branch.
trigger_request: trigger_request,
schedule: schedule,
merge_request: merge_request,
@@ -31,7 +36,10 @@ module Ci
seeds_block: block,
variables_attributes: params[:variables_attributes],
project: project,
- current_user: current_user)
+ current_user: current_user,
+ push_options: params[:push_options],
+ chat_data: params[:chat_data],
+ **extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
@@ -102,5 +110,14 @@ module Ci
pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def extra_options(options = {})
+ # In Ruby 2.4, even when options is empty, f(**options) doesn't work when f
+ # doesn't have any parameters. We reproduce the Ruby 2.5 behavior by
+ # checking explicitly that no arguments are given.
+ raise ArgumentError if options.any?
+
+ {} # overridden in EE
+ end
end
end
diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb
new file mode 100644
index 00000000000..7d2f5d33fed
--- /dev/null
+++ b/app/services/ci/destroy_expired_job_artifacts_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Ci
+ class DestroyExpiredJobArtifactsService
+ include ::Gitlab::ExclusiveLeaseHelpers
+ include ::Gitlab::LoopHelpers
+
+ BATCH_SIZE = 100
+ LOOP_TIMEOUT = 45.minutes
+ LOOP_LIMIT = 1000
+ EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock'
+ LOCK_TIMEOUT = 50.minutes
+
+ ##
+ # Destroy expired job artifacts on GitLab instance
+ #
+ # This destroy process cannot run for more than 45 minutes. This is for
+ # preventing multiple `ExpireBuildArtifactsWorker` CRON jobs run concurrently,
+ # which is scheduled at every hour.
+ def execute
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ destroy_batch
+ end
+ end
+ end
+
+ private
+
+ def destroy_batch
+ artifacts = Ci::JobArtifact.expired(BATCH_SIZE).to_a
+
+ return false if artifacts.empty?
+
+ artifacts.each(&:destroy!)
+ end
+ end
+end
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index 13f892aabb8..5c4a34043c1 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -5,8 +5,6 @@ module Ci
def execute(pipeline)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline)
- AuditEventService.new(current_user, pipeline).security_event
-
pipeline.destroy!
end
end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index f54574b026b..2dbb7c3917d 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -7,6 +7,8 @@ module Ci
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
+ elsif job_from_token
+ create_pipeline_from_job(job_from_token)
end
end
@@ -35,6 +37,14 @@ module Ci
end
end
+ def create_pipeline_from_job(job)
+ # overridden in EE
+ end
+
+ def job_from_token
+ # overridden in EE
+ end
+
def variables
params[:variables].to_h.map do |key, value|
{ key: key, value: value }
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 446188347df..4a7ce00b8e2 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -10,7 +10,7 @@ module Ci
update_retried
new_builds =
- stage_indexes_of_created_builds.map do |index|
+ stage_indexes_of_created_processables.map do |index|
process_stage(index)
end
@@ -27,7 +27,7 @@ module Ci
return if HasStatus::BLOCKED_STATUS.include?(current_status)
if HasStatus::COMPLETED_STATUSES.include?(current_status)
- created_builds_in_stage(index).select do |build|
+ created_processables_in_stage(index).select do |build|
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
Ci::ProcessBuildService.new(project, @user)
.execute(build, current_status)
@@ -43,19 +43,19 @@ module Ci
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def stage_indexes_of_created_builds
- created_builds.order(:stage_idx).pluck('distinct stage_idx')
+ def stage_indexes_of_created_processables
+ created_processables.order(:stage_idx).pluck('distinct stage_idx')
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def created_builds_in_stage(index)
- created_builds.where(stage_idx: index)
+ def created_processables_in_stage(index)
+ created_processables.where(stage_idx: index)
end
# rubocop: enable CodeReuse/ActiveRecord
- def created_builds
- pipeline.builds.created
+ def created_processables
+ pipeline.processables.created
end
# This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 218f1e63d08..fab8a179843 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -2,7 +2,7 @@
module Ci
class RetryBuildService < ::BaseService
- CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
+ CLONE_ACCESSORS = %i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request
yaml_variables when environment coverage_regex
description tag_list protected].freeze
diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb
index e86ca8cf1d0..8a71730d5ec 100644
--- a/app/services/clusters/applications/base_helm_service.rb
+++ b/app/services/clusters/applications/base_helm_service.rb
@@ -45,6 +45,10 @@ module Clusters
def install_command
@install_command ||= app.install_command
end
+
+ def upgrade_command(new_values = "")
+ app.upgrade_command(new_values)
+ end
end
end
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 21ec26ea233..c592d608b89 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -4,7 +4,7 @@ module Clusters
module Applications
class CheckInstallationProgressService < BaseHelmService
def execute
- return unless app.installing?
+ return unless operation_in_progress?
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
@@ -16,11 +16,16 @@ module Clusters
end
rescue Kubeclient::HttpError => e
log_error(e)
- app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored?
+
+ app.make_errored!("Kubernetes error: #{e.error_code}")
end
private
+ def operation_in_progress?
+ app.installing? || app.updating?
+ end
+
def on_success
app.make_installed!
ensure
@@ -28,13 +33,13 @@ module Clusters
end
def on_failed
- app.make_errored!("Installation failed. Check pod logs for #{install_command.pod_name} for more details.")
+ app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.")
end
def check_timeout
if timeouted?
begin
- app.make_errored!("Installation timed out. Check pod logs for #{install_command.pod_name} for more details.")
+ app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
end
else
ClusterWaitForAppInstallationWorker.perform_in(
@@ -42,20 +47,24 @@ module Clusters
end
end
+ def pod_name
+ install_command.pod_name
+ end
+
def timeouted?
Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
- helm_api.delete_pod!(install_command.pod_name)
+ helm_api.delete_pod!(pod_name)
end
def installation_phase
- helm_api.status(install_command.pod_name)
+ helm_api.status(pod_name)
end
def installation_errors
- helm_api.log(install_command.pod_name)
+ helm_api.log(pod_name)
end
end
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index 92c2c1b9834..12f8c849d41 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -27,9 +27,11 @@ module Clusters
application.oauth_application = create_oauth_application(application, request)
end
- application.save!
+ worker = application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker
- Clusters::Applications::ScheduleInstallationService.new(application).execute
+ application.make_scheduled!
+
+ worker.perform_async(application.name, application.id)
end
end
diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb
deleted file mode 100644
index d75ba70c27e..00000000000
--- a/app/services/clusters/applications/schedule_installation_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class ScheduleInstallationService
- attr_reader :application
-
- def initialize(application)
- @application = application
- end
-
- def execute
- application.make_scheduled!
-
- ClusterInstallAppWorker.perform_async(application.name, application.id)
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb
new file mode 100644
index 00000000000..a0ece1d2635
--- /dev/null
+++ b/app/services/clusters/applications/upgrade_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class UpgradeService < BaseHelmService
+ def execute
+ return unless app.scheduled?
+
+ begin
+ app.make_updating!
+
+ # install_command works with upgrades too
+ # as it basically does `helm upgrade --install`
+ helm_api.update(install_command)
+
+ ClusterWaitForAppInstallationWorker.perform_in(
+ ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
+ rescue Kubeclient::HttpError => e
+ log_error(e)
+ app.make_update_errored!("Kubernetes error: #{e.error_code}")
+ rescue StandardError => e
+ log_error(e)
+ app.make_update_errored!("Can't start upgrade process.")
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index 34593e12bd5..bb34a3d3352 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -11,6 +11,7 @@ module Commits
@start_project = params[:start_project] || @project
@start_branch = params[:start_branch]
@branch_name = params[:branch_name]
+ @force = params[:force] || false
end
def execute
@@ -42,10 +43,14 @@ module Commits
@start_branch != @branch_name || @start_project != @project
end
+ def force?
+ !!@force
+ end
+
def validate!
validate_permissions!
validate_on_branch!
- validate_branch_existance!
+ validate_branch_existence!
validate_new_branch_name! if different_branch?
end
@@ -64,14 +69,14 @@ module Commits
end
end
- def validate_branch_existance!
- if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name)
+ def validate_branch_existence!
+ if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) && !force?
raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes")
end
end
def validate_new_branch_name!
- result = ValidateNewBranchService.new(project, current_user).execute(@branch_name)
+ result = ValidateNewBranchService.new(project, current_user).execute(@branch_name, force: force?)
if result[:status] == :error
raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb
index 7961ba4d3c4..bb8cfb63f98 100644
--- a/app/services/commits/tag_service.rb
+++ b/app/services/commits/tag_service.rb
@@ -9,11 +9,10 @@ module Commits
tag_name = params[:tag_name]
message = params[:tag_message]
- release_description = nil
result = Tags::CreateService
.new(commit.project, current_user)
- .execute(tag_name, commit.sha, message, release_description)
+ .execute(tag_name, commit.sha, message)
if result[:status] == :success
tag = result[:tag]
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
index f102e00d150..2cb73555d85 100644
--- a/app/services/concerns/exclusive_lease_guard.rb
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -6,9 +6,14 @@
#
# `#try_obtain_lease` takes a block which will be run if it was able to
# obtain the lease. Implement `#lease_timeout` to configure the timeout
-# for the exclusive lease. Optionally override `#lease_key` to set the
+# for the exclusive lease.
+#
+# Optionally override `#lease_key` to set the
# lease key, it defaults to the class name with underscores.
#
+# Optionally override `#lease_release?` to prevent the job to
+# be re-executed more often than LEASE_TIMEOUT.
+#
module ExclusiveLeaseGuard
extend ActiveSupport::Concern
@@ -23,7 +28,7 @@ module ExclusiveLeaseGuard
begin
yield lease
ensure
- release_lease(lease)
+ release_lease(lease) if lease_release?
end
end
@@ -37,7 +42,11 @@ module ExclusiveLeaseGuard
def lease_timeout
raise NotImplementedError,
- "#{self.class.name} does not implement #{__method__}"
+ "#{self.class.name} does not implement #{__method__}"
+ end
+
+ def lease_release?
+ true
end
def release_lease(uuid)
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
index 5b408bd96c7..6713b6617ae 100644
--- a/app/services/concerns/users/participable_service.rb
+++ b/app/services/concerns/users/participable_service.rb
@@ -11,7 +11,7 @@ module Users
def noteable_owner
return [] unless noteable && noteable.author.present?
- [as_hash(noteable.author)]
+ [user_as_hash(noteable.author)]
end
def participants_in_noteable
@@ -23,21 +23,24 @@ module Users
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
- as_hash(user)
+ user_as_hash(user)
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
- count = group.users.count
- { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
+ group_as_hash(group)
end
end
private
- def as_hash(user)
- { username: user.username, name: user.name, avatar_url: user.avatar_url }
+ def user_as_hash(user)
+ { type: user.class.name, username: user.username, name: user.name, avatar_url: user.avatar_url }
+ end
+
+ def group_as_hash(group)
+ { type: group.class.name, username: group.full_path, name: group.full_name, avatar_url: group.avatar_url, count: group.users.count }
end
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 65208b07e27..110e589e30d 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class CreateBranchService < BaseService
- def execute(branch_name, ref)
- create_master_branch if project.empty_repo?
+ def execute(branch_name, ref, create_master_if_empty: true)
+ create_master_branch if create_master_if_empty && project.empty_repo?
result = ValidateNewBranchService.new(project, current_user)
.execute(branch_name)
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
deleted file mode 100644
index ab2dc5337aa..00000000000
--- a/app/services/create_release_service.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-class CreateReleaseService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(tag_name, release_description)
- repository = project.repository
- existing_tag = repository.find_tag(tag_name)
-
- # Only create a release if the tag exists
- if existing_tag
- release = project.releases.find_by(tag: tag_name)
-
- if release
- error('Release already exists', 409)
- else
- release = project.releases.create!(
- tag: tag_name,
- name: tag_name,
- sha: existing_tag.dereferenced_target.sha,
- author: current_user,
- description: release_description
- )
-
- success(release)
- end
- else
- error('Tag does not exist', 404)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def success(release)
- super().merge(release: release)
- end
-end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 44252f7b0a6..8322a3d74f4 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -14,7 +14,7 @@ class DeleteBranchService < BaseService
end
if repository.rm_branch(current_user, branch_name)
- success('Branch was removed')
+ success('Branch was deleted')
else
error('Failed to remove branch')
end
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index 988215ffc78..99324638300 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -2,10 +2,11 @@
module Emails
class BaseService
- attr_reader :current_user
+ attr_reader :current_user, :params, :user
def initialize(current_user, params = {})
- @current_user, @params = current_user, params.dup
+ @current_user = current_user
+ @params = params.dup
@user = params.delete(:user)
end
end
diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb
index 56925a724fe..dc06a5caa40 100644
--- a/app/services/emails/create_service.rb
+++ b/app/services/emails/create_service.rb
@@ -3,12 +3,11 @@
module Emails
class CreateService < ::Emails::BaseService
def execute(extra_params = {})
- skip_confirmation = @params.delete(:skip_confirmation)
+ skip_confirmation = params.delete(:skip_confirmation)
- email = @user.emails.create(@params.merge(extra_params))
-
- email&.confirm if skip_confirmation && current_user.admin?
- email
+ user.emails.create(params.merge(extra_params)).tap do |email|
+ email&.confirm if skip_confirmation && current_user.admin?
+ end
end
end
end
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
new file mode 100644
index 00000000000..a6c6bec9598
--- /dev/null
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ListIssuesService < ::BaseService
+ DEFAULT_ISSUE_STATUS = 'unresolved'
+ DEFAULT_LIMIT = 20
+
+ def execute
+ return error('Error Tracking is not enabled') unless enabled?
+ return error('Access denied', :unauthorized) unless can_read?
+
+ result = project_error_tracking_setting
+ .list_sentry_issues(issue_status: issue_status, limit: limit)
+
+ # our results are not yet ready
+ unless result
+ return error('Not ready. Try again later', :no_content)
+ end
+
+ if result[:error].present?
+ return error(result[:error], :bad_request)
+ end
+
+ success(issues: result[:issues])
+ end
+
+ def external_url
+ project_error_tracking_setting&.sentry_external_url
+ end
+
+ private
+
+ def project_error_tracking_setting
+ project.error_tracking_setting
+ end
+
+ def issue_status
+ params[:issue_status] || DEFAULT_ISSUE_STATUS
+ end
+
+ def limit
+ params[:limit] || DEFAULT_LIMIT
+ end
+
+ def enabled?
+ project_error_tracking_setting&.enabled?
+ end
+
+ def can_read?
+ can?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb
new file mode 100644
index 00000000000..4e92353a13c
--- /dev/null
+++ b/app/services/error_tracking/list_projects_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ListProjectsService < ::BaseService
+ def execute
+ return error('access denied') unless can_read?
+
+ setting = project_error_tracking_setting
+
+ unless setting.valid?
+ return error(setting.errors.full_messages.join(', '), :bad_request)
+ end
+
+ begin
+ result = setting.list_sentry_projects
+ rescue Sentry::Client::Error => e
+ return error(e.message, :bad_request)
+ rescue Sentry::Client::SentryError => e
+ return error(e.message, :unprocessable_entity)
+ end
+
+ success(projects: result[:projects])
+ end
+
+ private
+
+ def project_error_tracking_setting
+ (project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting|
+ setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
+ api_host: params[:api_host],
+ organization_slug: 'org',
+ project_slug: 'proj'
+ )
+
+ setting.token = params[:token]
+ setting.enabled = true
+ end
+ end
+
+ def can_read?
+ can?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 927634c2159..c1bc26c330a 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -46,7 +46,8 @@ module Files
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
- start_branch_name: @start_branch
+ start_branch_name: @start_branch,
+ force: force?
)
rescue ArgumentError => e
raise_error(e)
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index f1883877d56..f387c749a21 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -140,7 +140,7 @@ class GitPushService < BaseService
.perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(project, current_user, build_push_data)
- Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push)
+ Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options)
project.execute_hooks(build_push_data.dup, :push_hooks)
project.execute_services(build_push_data.dup, :push_hooks)
@@ -174,7 +174,8 @@ class GitPushService < BaseService
params[:newrev],
params[:ref],
@push_commits,
- commits_count: commits_count)
+ commits_count: commits_count,
+ push_options: params[:push_options] || [])
end
def push_to_existing_branch?
@@ -230,4 +231,10 @@ class GitPushService < BaseService
def last_pushed_commits
@last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT)
end
+
+ private
+
+ def pipeline_options
+ {} # to be overridden in EE
+ end
end
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index dbadafc0f52..e39b3603c6c 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -10,7 +10,7 @@ class GitTagPushService < BaseService
@push_data = build_push_data
EventCreateService.new.push(project, current_user, push_data)
- Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push)
+ Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options)
SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks)
project.execute_hooks(push_data.dup, :tag_push_hooks)
@@ -45,7 +45,8 @@ class GitTagPushService < BaseService
params[:newrev],
params[:ref],
commits,
- message)
+ message,
+ push_options: params[:push_options] || [])
end
def build_system_push_data
@@ -58,4 +59,8 @@ class GitTagPushService < BaseService
[],
'')
end
+
+ def pipeline_options
+ {} # to be overridden in EE
+ end
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 24d8400c625..99ead467f74 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -10,6 +10,8 @@ module Groups
def execute
@group = Group.new(params)
+ after_build_hook(@group, params)
+
unless can_use_visibility_level? && can_create_group?
return @group
end
@@ -30,6 +32,10 @@ module Groups
private
+ def after_build_hook(group, params)
+ # overridden in EE
+ end
+
def create_chat_team?
Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
end
diff --git a/app/services/groups/nested_create_service.rb b/app/services/groups/nested_create_service.rb
index f01f5656296..01bd685712b 100644
--- a/app/services/groups/nested_create_service.rb
+++ b/app/services/groups/nested_create_service.rb
@@ -12,7 +12,7 @@ module Groups
end
def execute
- return nil unless group_path
+ return unless group_path
if namespace = namespace_or_group(group_path)
return namespace
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index f64e327416a..94185605ab9 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -35,7 +35,10 @@ module Groups
def proceed_to_transfer
Group.transaction do
update_group_attributes
+ ensure_ownership
end
+
+ true
end
def ensure_allowed_transfer
@@ -95,6 +98,13 @@ module Groups
end
# rubocop: enable CodeReuse/ActiveRecord
+ def ensure_ownership
+ return if @new_parent_group
+ return unless @group.owners.empty?
+
+ @group.add_owner(current_user)
+ end
+
def raise_transfer_error(message)
raise TransferError, ERROR_MESSAGES[message]
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 31d3c844ad5..787445180f0 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -11,6 +11,8 @@ module Groups
return false unless valid_share_with_group_lock_change?
+ before_assignment_hook(group, params)
+
group.assign_attributes(params)
begin
@@ -28,10 +30,14 @@ module Groups
private
+ def before_assignment_hook(group, params)
+ # overridden in EE
+ end
+
def after_update
if group.previous_changes.include?(:visibility_level) && group.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::GroupPrivateWorker.perform_in(1.hour, group.id)
+ TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id)
end
end
diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb
new file mode 100644
index 00000000000..2683c75e41f
--- /dev/null
+++ b/app/services/import/base_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Import
+ class BaseService < ::BaseService
+ def initialize(client, user, params)
+ @client = client
+ @current_user = user
+ @params = params
+ end
+
+ private
+
+ def find_or_create_namespace(namespace, owner)
+ namespace = params[:target_namespace].presence || namespace
+
+ return current_user.namespace if namespace == owner
+
+ group = Groups::NestedCreateService.new(current_user, group_path: namespace).execute
+
+ group.errors.any? ? current_user.namespace : group
+ rescue => e
+ Gitlab::AppLogger.error(e)
+
+ current_user.namespace
+ end
+
+ def project_save_error(project)
+ project.errors.full_messages.join(', ')
+ end
+
+ def success(project)
+ super().merge(project: project, status: :success)
+ end
+ end
+end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
new file mode 100644
index 00000000000..a2533683da9
--- /dev/null
+++ b/app/services/import/github_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Import
+ class GithubService < Import::BaseService
+ attr_accessor :client
+ attr_reader :params, :current_user
+
+ def execute(access_params, provider)
+ unless authorized?
+ return error('This namespace has already been taken! Please choose another one.', :unprocessable_entity)
+ end
+
+ project = Gitlab::LegacyGithubImport::ProjectCreator
+ .new(repo, project_name, target_namespace, current_user, access_params, type: provider)
+ .execute(extra_project_attrs)
+
+ if project.persisted?
+ success(project)
+ else
+ error(project_save_error(project), :unprocessable_entity)
+ end
+ end
+
+ def repo
+ @repo ||= client.repo(params[:repo_id].to_i)
+ end
+
+ def project_name
+ @project_name ||= params[:new_name].presence || repo.name
+ end
+
+ def namespace_path
+ @namespace_path ||= params[:target_namespace].presence || current_user.namespace_path
+ end
+
+ def target_namespace
+ @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path)
+ end
+
+ def extra_project_attrs
+ {}
+ end
+
+ def authorized?
+ can?(current_user, :create_projects, target_namespace)
+ end
+ end
+end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 885e14bba8f..77f38f8882e 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -20,7 +20,7 @@ module Issuable
create_due_date_note if issuable.previous_changes.include?('due_date')
create_milestone_note if issuable.previous_changes.include?('milestone_id')
- create_labels_note(old_labels) if issuable.labels != old_labels
+ create_labels_note(old_labels) if old_labels && issuable.labels != old_labels
end
private
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index c7e7bb55e4b..f35ad2a9d8b 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -61,10 +61,10 @@ class IssuableBaseService < BaseService
return unless milestone_id
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
- group_ids = project.group&.self_and_ancestors&.pluck(:id)
+ groups = project.group&.self_and_ancestors&.select(:id)
milestone =
- Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id)
+ Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id)
params[:milestone_id] = '' unless milestone
end
@@ -89,7 +89,7 @@ class IssuableBaseService < BaseService
return unless labels
- params[:label_ids] = labels.split(",").map do |label_name|
+ params[:label_ids] = labels.map do |label_name|
label = Labels::FindOrCreateService.new(
current_user,
parent,
@@ -235,6 +235,61 @@ class IssuableBaseService < BaseService
issuable
end
+ def update_task(issuable)
+ filter_params(issuable)
+
+ if issuable.changed? || params.present?
+ issuable.assign_attributes(params.merge(updated_by: current_user,
+ last_edited_at: Time.now,
+ last_edited_by: current_user))
+
+ before_update(issuable)
+
+ if issuable.with_transaction_returning_status { issuable.save }
+ # We do not touch as it will affect a update on updated_at field
+ ActiveRecord::Base.no_touching do
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: nil)
+ end
+
+ handle_task_changes(issuable)
+ invalidate_cache_counts(issuable, users: issuable.assignees.to_a)
+ after_update(issuable)
+ execute_hooks(issuable, 'update', old_associations: nil)
+ end
+ end
+
+ issuable
+ end
+
+ # Handle the `update_task` event sent from UI. Attempts to update a specific
+ # line in the markdown and cached html, bypassing any unnecessary updates or checks.
+ def update_task_event(issuable)
+ update_task_params = params.delete(:update_task)
+ return unless update_task_params
+
+ tasklist_toggler = TaskListToggleService.new(issuable.description, issuable.description_html,
+ line_source: update_task_params[:line_source],
+ line_number: update_task_params[:line_number].to_i,
+ toggle_as_checked: update_task_params[:checked])
+
+ unless tasklist_toggler.execute
+ # if we make it here, the data is much newer than we thought it was - fail fast
+ raise ActiveRecord::StaleObjectError
+ end
+
+ # by updating the description_html field at the same time,
+ # the markdown cache won't be considered invalid
+ params[:description] = tasklist_toggler.updated_markdown
+ params[:description_html] = tasklist_toggler.updated_markdown_html
+
+ # since we're updating a very specific line, we don't care whether
+ # the `lock_version` sent from the FE is the same or not. Just
+ # make sure the data hasn't changed since we queried it
+ params[:lock_version] = issuable.lock_version
+
+ update_task(issuable)
+ end
+
def labels_changing?(old_label_ids, new_label_ids)
old_label_ids.sort != new_label_ids.sort
end
@@ -318,6 +373,10 @@ class IssuableBaseService < BaseService
end
# override if needed
+ def handle_task_changes(issuable)
+ end
+
+ # override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
@@ -328,4 +387,10 @@ class IssuableBaseService < BaseService
def parent
project
end
+
+ # we need to check this because milestone from milestone_id param is displayed on "new" page
+ # where private project milestone could leak without this check
+ def ensure_milestone_available(issuable)
+ issuable.milestone_id = nil unless issuable.milestone_available?
+ end
end
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 52b45f1b2ce..61615ac2058 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -6,7 +6,9 @@ module Issues
def execute
filter_resolve_discussion_params
- @issue = project.issues.new(issue_params)
+ @issue = project.issues.new(issue_params).tap do |issue|
+ ensure_milestone_available(issue)
+ end
end
def issue_params_with_info_from_discussions
@@ -57,9 +59,11 @@ module Issues
end
def issue_params
- @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
+ @issue_params ||= build_issue_params
end
+ private
+
def whitelisted_issue_params
if can?(current_user, :admin_issue, project)
params.slice(:title, :description, :milestone_id)
@@ -67,5 +71,9 @@ module Issues
params.slice(:title, :description)
end
end
+
+ def build_issue_params
+ issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
+ end
end
end
diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb
new file mode 100644
index 00000000000..ef08fafa7cc
--- /dev/null
+++ b/app/services/issues/import_csv_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Issues
+ class ImportCsvService
+ def initialize(user, project, csv_io)
+ @user = user
+ @project = project
+ @csv_io = csv_io
+ @results = { success: 0, error_lines: [], parse_error: false }
+ end
+
+ def execute
+ process_csv
+ email_results_to_user
+
+ @results
+ end
+
+ private
+
+ def process_csv
+ csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
+
+ CSV.new(csv_data, col_sep: detect_col_sep(csv_data.lines.first), headers: true).each.with_index(2) do |row, line_no|
+ issue = Issues::CreateService.new(@project, @user, title: row[0], description: row[1]).execute
+
+ if issue.persisted?
+ @results[:success] += 1
+ else
+ @results[:error_lines].push(line_no)
+ end
+ end
+ rescue ArgumentError, CSV::MalformedCSVError
+ @results[:parse_error] = true
+ end
+
+ def email_results_to_user
+ Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_now
+ end
+
+ def detect_col_sep(header)
+ if header.include?(",")
+ ","
+ elsif header.include?(";")
+ ";"
+ elsif header.include?("\t")
+ "\t"
+ else
+ raise CSV::MalformedCSVError
+ end
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a1d0cc0e568..cec5b5734c0 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -8,7 +8,7 @@ module Issues
handle_move_between_ids(issue)
filter_spam_check_params
change_issue_duplicate(issue)
- move_issue_to_new_project(issue) || update(issue)
+ move_issue_to_new_project(issue) || update_task_event(issue) || update(issue)
end
def update(issue)
@@ -44,7 +44,7 @@ module Issues
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::ConfidentialIssueWorker.perform_in(1.hour, issue.id) if issue.confidential?
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
end
@@ -63,6 +63,11 @@ module Issues
end
end
+ def handle_task_changes(issuable)
+ todo_service.mark_pending_todos_as_done(issuable, current_user)
+ todo_service.update_issue(issuable, current_user)
+ end
+
def handle_move_between_ids(issue)
return unless params[:move_between_ids]
@@ -78,6 +83,8 @@ module Issues
# rubocop: disable CodeReuse/ActiveRecord
def change_issue_duplicate(issue)
canonical_issue_id = params.delete(:canonical_issue_id)
+ return unless canonical_issue_id
+
canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
if canonical_issue
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
index fe34be41ac1..db710bac900 100644
--- a/app/services/labels/create_service.rb
+++ b/app/services/labels/create_service.rb
@@ -3,7 +3,7 @@
module Labels
class CreateService < Labels::BaseService
def initialize(params = {})
- @params = params.dup.with_indifferent_access
+ @params = params.to_h.dup.with_indifferent_access
end
# returns the created label
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index 3c0e6196d4f..e73e6476c12 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -49,7 +49,7 @@ module Labels
.new(current_user, title: new_label.title, group_id: project.group.id)
.execute(skip_authorization: true)
.where.not(id: new_label)
- .select(:id) # Can't use pluck() to avoid object-creation because of the batching
+ .select(:id) # Can't use pluck() to avoid object-creation because of the batching
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
index c3a720a1c66..be33947d0eb 100644
--- a/app/services/labels/update_service.rb
+++ b/app/services/labels/update_service.rb
@@ -3,11 +3,12 @@
module Labels
class UpdateService < Labels::BaseService
def initialize(params = {})
- @params = params.dup.with_indifferent_access
+ @params = params.to_h.dup.with_indifferent_access
end
# returns the updated label
def execute(label)
+ params[:name] = params.delete(:new_name) if params.key?(:new_name)
params[:color] = convert_color_name_to_hex if params[:color].present?
label.update(params)
diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb
index 4a5b2a52921..192ce3d3c2a 100644
--- a/app/services/lfs/locks_finder_service.rb
+++ b/app/services/lfs/locks_finder_service.rb
@@ -12,7 +12,7 @@ module Lfs
# rubocop: disable CodeReuse/ActiveRecord
def find_locks
- options = params.slice(:id, :path).compact.symbolize_keys
+ options = params.slice(:id, :path).to_h.compact.symbolize_keys
project.lfs_file_locks.where(options)
end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index d734571f835..e78affff797 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -47,5 +47,11 @@ module Members
raise "Unknown action '#{action}' on #{member}!"
end
end
+
+ def enqueue_delete_todos(member)
+ type = member.is_a?(GroupMember) ? 'Group' : 'Project'
+ # don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type)
+ end
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 714b8586737..cf710fef52b 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -19,9 +19,19 @@ module Members
current_user: current_user
)
- members.each { |member| after_execute(member: member) }
+ errors = []
- success
+ members.each do |member|
+ if member.errors.any?
+ errors << "#{member.user.username}: #{member.errors.full_messages.to_sentence}"
+ else
+ after_execute(member: member)
+ end
+ end
+
+ return success unless errors.any?
+
+ error(errors.to_sentence)
end
private
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index c186a5971dc..f9717a9426b 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,9 +2,11 @@
module Members
class DestroyService < Members::BaseService
- def execute(member, skip_authorization: false)
+ def execute(member, skip_authorization: false, skip_subresources: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
+ @skip_auth = skip_authorization
+
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy
@@ -15,7 +17,8 @@ module Members
notification_service.decline_access_request(member)
end
- enqeue_delete_todos(member)
+ delete_subresources(member) unless skip_subresources
+ enqueue_delete_todos(member)
after_execute(member: member)
@@ -24,10 +27,27 @@ module Members
private
- def enqeue_delete_todos(member)
- type = member.is_a?(GroupMember) ? 'Group' : 'Project'
- # don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::EntityLeaveWorker.perform_in(1.hour, member.user_id, member.source_id, type)
+ def delete_subresources(member)
+ return unless member.is_a?(GroupMember) && member.user && member.group
+
+ delete_project_members(member)
+ delete_subgroup_members(member) if Group.supports_nested_objects?
+ end
+
+ def delete_project_members(member)
+ groups = member.group.self_and_descendants
+
+ ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member|
+ self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
+ end
+ end
+
+ def delete_subgroup_members(member)
+ groups = member.group.descendants
+
+ GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
+ self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
+ end
end
def can_destroy_member?(member)
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 1f5618dae53..ff8d5c1d8c9 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -10,9 +10,18 @@ module Members
if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
+
+ # Deletes only confidential issues todos for guests
+ enqueue_delete_todos(member) if downgrading_to_guest?
end
member
end
+
+ private
+
+ def downgrading_to_guest?
+ params[:access_level] == Gitlab::Access::GUEST
+ end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index fe19abf50f6..11ede5223e5 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -63,10 +63,11 @@ module MergeRequests
# UpdateMergeRequestsWorker could be retried by an exception.
# MR pipelines should not be recreated in such case.
return if merge_request.merge_request_pipeline_exists?
+ return if merge_request.has_no_commits?
Ci::CreatePipelineService
.new(merge_request.source_project, user, ref: merge_request.source_branch)
- .execute(:merge_request,
+ .execute(:merge_request_event,
ignore_skip_ci: true,
save_on_errors: false,
merge_request: merge_request)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 36767621d74..109c964e577 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -18,7 +18,8 @@ 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?
+ merge_request.can_be_created = projects_and_branches_valid?
+ ensure_milestone_available(merge_request)
# compare branches only if branches are valid, otherwise
# compare_branches may raise an error
@@ -49,15 +50,19 @@ module MergeRequests
to: :merge_request
def find_source_project
- return source_project if source_project.present? && can?(current_user, :read_project, source_project)
+ return source_project if source_project.present? && can?(current_user, :create_merge_request_from, source_project)
project
end
def find_target_project
- return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+ return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
- project.default_merge_request_target
+ target_project = project.default_merge_request_target
+
+ return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
+
+ project
end
def find_target_branch
@@ -72,10 +77,11 @@ module MergeRequests
params[:target_branch].present?
end
- def branches_valid?
+ def projects_and_branches_valid?
+ return false if source_project.nil? || target_project.nil?
return false unless source_branch_specified? || target_branch_specified?
- validate_branches
+ validate_projects_and_branches
errors.blank?
end
@@ -94,7 +100,12 @@ module MergeRequests
end
end
- def validate_branches
+ def validate_projects_and_branches
+ merge_request.validate_target_project
+ merge_request.validate_fork
+
+ return if errors.any?
+
add_error('You must select source and target branch') unless branches_present?
add_error('You must select different branches') if same_source_and_target?
add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 7bb9fa60515..02c2388c05c 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -26,7 +26,7 @@ module MergeRequests
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
create_merge_request_pipeline(issuable, current_user)
- update_merge_requests_head_pipeline(issuable)
+ issuable.update_head_pipeline
super
end
@@ -45,20 +45,6 @@ module MergeRequests
private
- def update_merge_requests_head_pipeline(merge_request)
- pipeline = head_pipeline_for(merge_request)
- merge_request.update(head_pipeline_id: pipeline.id) if pipeline
- end
-
- def head_pipeline_for(merge_request)
- return unless merge_request.source_project
-
- sha = merge_request.source_branch_sha
- return unless sha
-
- merge_request.all_pipelines(shas: sha).first
- end
-
def set_projects!
# @project is used to determine whether the user can set the merge request's
# assignee, milestone and labels. Whether they can depends on their
diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
new file mode 100644
index 00000000000..095bdca5472
--- /dev/null
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class MergeBaseService < MergeRequests::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ MergeError = Class.new(StandardError)
+
+ attr_reader :merge_request
+
+ # Overridden in EE.
+ def hooks_validation_pass?(_merge_request)
+ true
+ end
+
+ # Overridden in EE.
+ def hooks_validation_error(_merge_request)
+ # No-op
+ end
+
+ def source
+ if merge_request.squash
+ squash_sha!
+ else
+ merge_request.diff_head_sha
+ end
+ end
+
+ private
+
+ # Overridden in EE.
+ def error_check!
+ # No-op
+ end
+
+ def raise_error(message)
+ raise MergeError, message
+ end
+
+ def handle_merge_error(*args)
+ # No-op
+ end
+
+ def commit_message
+ params[:commit_message] ||
+ merge_request.default_merge_commit_message
+ end
+
+ def squash_sha!
+ strong_memoize(:squash_sha) do
+ params[:merge_request] = merge_request
+ squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute
+
+ case squash_result[:status]
+ when :success
+ squash_result[:squash_sha]
+ when :error
+ raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 70a67baa01c..b29e0b1759e 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -7,11 +7,7 @@ module MergeRequests
# mark merge request as merged and execute all hooks and notifications
# Executed when you do merge via GitLab UI
#
- class MergeService < MergeRequests::BaseService
- MergeError = Class.new(StandardError)
-
- attr_reader :merge_request, :source
-
+ class MergeService < MergeRequests::MergeBaseService
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request)
@@ -22,7 +18,7 @@ module MergeRequests
@merge_request = merge_request
- error_check!
+ validate!
merge_request.in_locked_state do
if commit
@@ -36,27 +32,22 @@ module MergeRequests
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
- def source
- return merge_request.diff_head_sha unless merge_request.squash
-
- squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)
+ private
- case squash_result[:status]
- when :success
- squash_result[:squash_sha]
- when :error
- raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
- end
+ def validate!
+ authorization_check!
+ error_check!
end
- # Overridden in EE.
- def hooks_validation_pass?(_merge_request)
- true
+ def authorization_check!
+ unless @merge_request.can_be_merged_by?(current_user)
+ raise_error('You are not allowed to merge this merge request')
+ end
end
- private
-
def error_check!
+ super
+
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
@@ -66,7 +57,7 @@ module MergeRequests
'No source for merge'
end
- raise MergeError, error if error
+ raise_error(error) if error
end
def commit
@@ -76,22 +67,19 @@ module MergeRequests
if commit_id
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
else
- raise MergeError, 'Conflicts detected during merge'
+ raise_error('Conflicts detected during merge')
end
merge_request.update!(merge_commit_sha: commit_id)
end
def try_merge
- message = params[:commit_message] || merge_request.merge_commit_message
-
- repository.merge(current_user, source, merge_request, message)
+ repository.merge(current_user, source, merge_request, commit_message)
rescue Gitlab::Git::PreReceiveError => e
- handle_merge_error(log_message: e.message)
- raise MergeError, 'Something went wrong during merge pre-receive hook'
+ raise_error("Something went wrong during merge pre-receive hook: #{e.message}")
rescue => e
handle_merge_error(log_message: e.message)
- raise MergeError, 'Something went wrong during merge'
+ raise_error('Something went wrong during merge')
ensure
merge_request.update!(in_progress_merge_commit_sha: nil)
end
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
new file mode 100644
index 00000000000..586652ae44e
--- /dev/null
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ # Performs the merge between source SHA and the target branch. Instead
+ # of writing the result to the MR target branch, it targets the `target_ref`.
+ #
+ # Ideally this should leave the `target_ref` state with the same state the
+ # target branch would have if we used the regular `MergeService`, but without
+ # every side-effect that comes with it (MR updates, mails, source branch
+ # deletion, etc). This service should be kept idempotent (i.e. can
+ # be executed regardless of the `target_ref` current state).
+ #
+ class MergeToRefService < MergeRequests::MergeBaseService
+ def execute(merge_request)
+ @merge_request = merge_request
+
+ validate!
+
+ commit_id = commit
+
+ raise_error('Conflicts detected during merge') unless commit_id
+
+ success(commit_id: commit_id)
+ rescue MergeError => error
+ error(error.message)
+ end
+
+ private
+
+ def validate!
+ authorization_check!
+ error_check!
+ end
+
+ def error_check!
+ super
+
+ error =
+ if Feature.disabled?(:merge_to_tmp_merge_ref_path, project)
+ 'Feature is not enabled'
+ elsif !merge_method_supported?
+ "#{project.human_merge_method} to #{target_ref} is currently not supported."
+ elsif !hooks_validation_pass?(merge_request)
+ hooks_validation_error(merge_request)
+ elsif @merge_request.should_be_rebased?
+ 'Fast-forward merge is not possible. Please update your source branch.'
+ elsif !@merge_request.mergeable_to_ref?
+ "Merge request is not mergeable to #{target_ref}"
+ elsif !source
+ 'No source for merge'
+ end
+
+ raise_error(error) if error
+ end
+
+ def authorization_check!
+ unless Ability.allowed?(current_user, :admin_merge_request, project)
+ raise_error("You are not allowed to merge to this ref")
+ end
+ end
+
+ def target_ref
+ merge_request.merge_ref_path
+ end
+
+ def commit
+ repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message)
+ rescue Gitlab::Git::PreReceiveError => error
+ raise MergeError, error.message
+ end
+
+ def merge_method_supported?
+ [:merge, :rebase_merge].include?(project.merge_method)
+ end
+ end
+end
diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb
index a439a380255..9d1a5d5e6d4 100644
--- a/app/services/merge_requests/squash_service.rb
+++ b/app/services/merge_requests/squash_service.rb
@@ -2,15 +2,10 @@
module MergeRequests
class SquashService < MergeRequests::WorkingCopyBaseService
- def execute(merge_request)
- @merge_request = merge_request
- @repository = target_project.repository
-
- squash || error('Failed to squash. Should be done manually.')
- end
-
- def squash
- if merge_request.commits_count < 2
+ def execute
+ # If performing a squash would result in no change, then
+ # immediately return a success message without performing a squash
+ if merge_request.commits_count < 2 && message.nil?
return success(squash_sha: merge_request.diff_head_sha)
end
@@ -18,7 +13,13 @@ module MergeRequests
return error('Squash task canceled: another squash is already in progress.')
end
- squash_sha = repository.squash(current_user, merge_request)
+ squash! || error('Failed to squash. Should be done manually.')
+ end
+
+ private
+
+ def squash!
+ squash_sha = repository.squash(current_user, merge_request, message || merge_request.default_squash_commit_message)
success(squash_sha: squash_sha)
rescue => e
@@ -26,5 +27,17 @@ module MergeRequests
log_error(e.message)
false
end
+
+ def repository
+ target_project.repository
+ end
+
+ def merge_request
+ params[:merge_request]
+ end
+
+ def message
+ params[:squash_commit_message].presence
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 33d8299c8b6..8112c2a4299 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -21,7 +21,7 @@ module MergeRequests
end
handle_wip_event(merge_request)
- update(merge_request)
+ update_task_event(merge_request) || update(merge_request)
end
# rubocop:disable Metrics/AbcSize
@@ -46,11 +46,13 @@ module MergeRequests
end
if merge_request.previous_changes.include?('assignee_id')
+ reassigned_merge_request_args = [merge_request, current_user]
+
old_assignee_id = merge_request.previous_changes['assignee_id'].first
- old_assignee = User.find(old_assignee_id) if old_assignee_id
+ reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
create_assignee_note(merge_request)
- notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignee)
+ notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
todo_service.reassigned_merge_request(merge_request, current_user)
end
@@ -81,6 +83,11 @@ module MergeRequests
end
# rubocop:enable Metrics/AbcSize
+ def handle_task_changes(merge_request)
+ todo_service.mark_pending_todos_as_done(merge_request, current_user)
+ todo_service.update_merge_request(merge_request, current_user)
+ end
+
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha)
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index 39071b5dc14..cbe5996e8ca 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -82,11 +82,9 @@ module Milestones
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def group_project_ids
- @group_project_ids ||= group.projects.pluck(:id)
+ group.projects.select(:id)
end
- # rubocop: enable CodeReuse/ActiveRecord
def raise_error(message)
raise PromoteMilestoneError, "Promotion failed - #{message}"
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
index 7b92fe6fe14..541f3e0d23c 100644
--- a/app/services/notes/build_service.rb
+++ b/app/services/notes/build_service.rb
@@ -9,12 +9,14 @@ module Notes
if in_reply_to_discussion_id.present?
discussion = find_discussion(in_reply_to_discussion_id)
- unless discussion
+ unless discussion && can?(current_user, :create_note, discussion.noteable)
note = Note.new
note.errors.add(:base, 'Discussion to reply to cannot be found')
return note
end
+ discussion = discussion.convert_to_discussion! if discussion.can_convert_to_discussion?
+
params.merge!(discussion.reply_attributes)
should_resolve = discussion.resolved?
end
@@ -34,19 +36,8 @@ module Notes
if project
project.notes.find_discussion(discussion_id)
else
- discussion = Note.find_discussion(discussion_id)
- noteable = discussion.noteable
-
- return nil unless noteable_without_project?(noteable)
-
- discussion
+ Note.find_discussion(discussion_id)
end
end
-
- def noteable_without_project?(noteable)
- return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
-
- false
- end
end
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index c4546f30235..1b46f6d8a72 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -21,7 +21,7 @@ module Notes
if quick_actions_service.supported?(note)
options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
- content, command_params = quick_actions_service.extract_commands(note, options)
+ content, update_params = quick_actions_service.execute(note, options)
only_commands = content.empty?
@@ -34,21 +34,26 @@ module Notes
end
if !only_commands && note.save
+ if note.part_of_discussion? && note.discussion.can_convert_to_discussion?
+ note.discussion.convert_to_discussion!(save: true)
+ end
+
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
end
- if command_params.present?
- quick_actions_service.execute(command_params, note)
+ if quick_actions_service.commands_executed_count.to_i > 0
+ if update_params.present?
+ quick_actions_service.apply_updates(update_params, note)
+ note.commands_changes = update_params
+ end
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
note.errors.add(:commands_only, 'Commands applied')
end
-
- note.commands_changes = command_params
end
note
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 4c14d834949..0852a708240 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -1,15 +1,31 @@
# frozen_string_literal: true
+# QuickActionsService class
+#
+# Executes quick actions commands extracted from note text
+#
+# Most commands returns parameters to be applied later
+# using QuickActionService#apply_updates
+#
module Notes
class QuickActionsService < BaseService
+ attr_reader :interpret_service
+
+ delegate :commands_executed_count, to: :interpret_service, allow_nil: true
+
UPDATE_SERVICES = {
'Issue' => Issues::UpdateService,
'MergeRequest' => MergeRequests::UpdateService,
'Commit' => Commits::TagService
}.freeze
+ private_constant :UPDATE_SERVICES
+
+ def self.update_services
+ UPDATE_SERVICES
+ end
def self.noteable_update_service(note)
- UPDATE_SERVICES[note.noteable_type]
+ update_services[note.noteable_type]
end
def self.supported?(note)
@@ -20,18 +36,21 @@ module Notes
self.class.supported?(note)
end
- def extract_commands(note, options = {})
+ def execute(note, options = {})
return [note.note, {}] unless supported?(note)
- QuickActions::InterpretService.new(project, current_user, options)
- .execute(note.note, note.noteable)
+ @interpret_service = QuickActions::InterpretService.new(project, current_user, options)
+
+ @interpret_service.execute(note.note, note.noteable)
end
- def execute(command_params, note)
- return if command_params.empty?
+ # Applies updates extracted to note#noteable
+ # The update parameters are extracted on self#execute
+ def apply_updates(update_params, note)
+ return if update_params.empty?
return unless supported?(note)
- self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
+ self.class.noteable_update_service(note).new(note.parent, current_user, update_params).execute(note.noteable)
end
end
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 68cdc69023a..56f11b31110 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -249,6 +249,7 @@ module NotificationRecipientService
attr_reader :action
attr_reader :previous_assignee
attr_reader :skip_current_user
+
def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target
@current_user = current_user
@@ -258,9 +259,13 @@ module NotificationRecipientService
@skip_current_user = skip_current_user
end
+ def add_watchers
+ add_project_watchers
+ end
+
def build!
add_participants(current_user)
- add_project_watchers
+ add_watchers
add_custom_notifications
# Re-assign is considered as a mention of the new assignee
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ff035fea216..1a65561dd70 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -188,7 +188,7 @@ class NotificationService
# * merge_request assignee if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user, previous_assignee)
+ def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
@@ -373,7 +373,8 @@ class NotificationService
end
def project_was_moved(project, old_path_with_namespace)
- recipients = notifiable_users(project.team.members, :mention, project: project)
+ recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
+ recipients = notifiable_users(recipients, :mention, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index a449a5dc3e9..c1655c38095 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -10,8 +10,7 @@ class PreviewMarkdownService < BaseService
text: text,
users: users,
suggestions: suggestions,
- commands: commands.join(' '),
- markdown_engine: markdown_engine
+ commands: commands.join(' ')
)
end
@@ -49,12 +48,4 @@ class PreviewMarkdownService < BaseService
def commands_target_id
params[:quick_actions_target_id]
end
-
- def markdown_engine
- if params[:legacy_render]
- :redcarpet
- else
- CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
- end
- end
end
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index aa9b253eb20..fafdecb3222 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -12,22 +12,27 @@ module Projects
#
# Projects::AfterRenameService.new(project).execute
class AfterRenameService
- attr_reader :project, :full_path_before, :full_path_after, :path_before
+ # @return [String] The Project being renamed.
+ attr_reader :project
- RenameFailedError = Class.new(StandardError)
+ # @return [String] The path slug the project was using, before the rename took place.
+ attr_reader :path_before
- # @param [Project] project The Project of the repository to rename.
- def initialize(project)
- @project = project
+ # @return [String] The full path of the namespace + project, before the rename took place.
+ attr_reader :full_path_before
- # The full path of the namespace + project, before the rename took place.
- @full_path_before = project.full_path_was
+ # @return [String] The full path of the namespace + project, after the rename took place.
+ attr_reader :full_path_after
- # The full path of the namespace + project, after the rename took place.
- @full_path_after = project.build_full_path
+ RenameFailedError = Class.new(StandardError)
- # The path of just the project, before the rename took place.
- @path_before = project.path_was
+ # @param [Project] project The Project being renamed.
+ # @param [String] path_before The path slug the project was using, before the rename took place.
+ def initialize(project, path_before:, full_path_before:)
+ @project = project
+ @path_before = path_before
+ @full_path_before = full_path_before
+ @full_path_after = project.full_path
end
def execute
@@ -57,11 +62,11 @@ module Projects
def rename_or_migrate_repository!
success =
if migrate_to_hashed_storage?
- ::Projects::HashedStorageMigrationService
+ ::Projects::HashedStorage::MigrationService
.new(project, full_path_before)
.execute
else
- project.storage.rename_repo
+ project.storage.rename_repo(old_full_path: full_path_before, new_full_path: full_path_after)
end
rename_failed! unless success
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 61f6402a810..3dad90188cf 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -14,7 +14,7 @@ module Projects
order: { due_date: :asc, title: :asc }
}
- finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group
+ finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group
MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
new file mode 100644
index 00000000000..488290db824
--- /dev/null
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ class CleanupTagsService < BaseService
+ def execute(container_repository)
+ return error('feature disabled') unless can_use?
+ return error('access denied') unless can_admin?
+
+ tags = container_repository.tags
+ tags_by_digest = group_by_digest(tags)
+
+ tags = without_latest(tags)
+ tags = filter_by_name(tags)
+ tags = with_manifest(tags)
+ tags = order_by_date(tags)
+ tags = filter_keep_n(tags)
+ tags = filter_by_older_than(tags)
+
+ deleted_tags = delete_tags(tags, tags_by_digest)
+
+ success(deleted: deleted_tags.map(&:name))
+ end
+
+ private
+
+ def delete_tags(tags_to_delete, tags_by_digest)
+ deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags|
+ delete_tag_digest(digest, tags, tags_by_digest[digest])
+ end
+
+ deleted_digests.values.flatten
+ end
+
+ def delete_tag_digest(digest, tags, other_tags)
+ # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/21405
+ # we have to remove all tags due
+ # to Docker Distribution bug unable
+ # to delete single tag
+ return unless tags.count == other_tags.count
+
+ # delete all tags
+ tags.map(&:delete)
+ end
+
+ def group_by_digest(tags)
+ tags.group_by(&:digest)
+ end
+
+ def without_latest(tags)
+ tags.reject(&:latest?)
+ end
+
+ def with_manifest(tags)
+ tags.select(&:valid?)
+ end
+
+ def order_by_date(tags)
+ now = DateTime.now
+ tags.sort_by { |tag| tag.created_at || now }.reverse
+ end
+
+ def filter_by_name(tags)
+ regex = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex']}\\z")
+
+ tags.select do |tag|
+ regex.scan(tag.name).any?
+ end
+ end
+
+ def filter_keep_n(tags)
+ tags.drop(params['keep_n'].to_i)
+ end
+
+ def filter_by_older_than(tags)
+ return tags unless params['older_than']
+
+ older_than = ChronicDuration.parse(params['older_than']).seconds.ago
+
+ tags.select do |tag|
+ tag.created_at && tag.created_at < older_than
+ end
+ end
+
+ def can_admin?
+ can?(current_user, :admin_container_image, project)
+ end
+
+ def can_use?
+ Feature.enabled?(:container_registry_cleanup, project, default_enabled: true)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index 8306d43ca7c..678bc0d24c3 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -5,7 +5,7 @@ module Projects
include Gitlab::Utils::StrongMemoize
def initialize(user, params)
- @current_user, @params = user, params.dup
+ @current_user, @params = user, params.to_h.dup
end
def execute
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 336d029d330..b14b31302f5 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -7,9 +7,16 @@ module Projects
DestroyError = Class.new(StandardError)
DELETED_FLAG = '+deleted'.freeze
+ REPO_REMOVAL_DELAY = 5.minutes.to_i
def async_execute
project.update_attribute(:pending_delete, true)
+
+ # Ensure no repository +deleted paths are kept,
+ # regardless of any issue with the ProjectDestroyWorker
+ # job process.
+ schedule_stale_repos_removal
+
job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}")
end
@@ -92,14 +99,23 @@ module Projects
log_info(%Q{Repository "#{path}" moved to "#{new_path}" for project "#{project.full_path}"})
project.run_after_commit do
- # self is now project
- GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path)
+ GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY, :remove_repository, self.repository_storage, new_path)
end
else
false
end
end
+ def schedule_stale_repos_removal
+ repo_paths = [removal_path(repo_path), removal_path(wiki_path)]
+
+ # Ideally it should wait until the regular removal phase finishes,
+ # so let's delay it a bit further.
+ repo_paths.each do |path|
+ GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY * 2, :remove_repository, project.repository_storage, path)
+ end
+ end
+
def rollback_repository(old_path, new_path)
# There is a possibility project does not have repository or wiki
return true unless repo_exists?(old_path)
@@ -113,13 +129,11 @@ module Projects
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def mv_repository(from_path, to_path)
- return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
+ return true unless repo_exists?(from_path)
gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
end
- # rubocop: enable CodeReuse/ActiveRecord
def attempt_rollback(project, message)
return unless project
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index dd297c9ba43..aba175eb79b 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -11,7 +11,7 @@ module Projects
end
def execute
- return nil unless valid_url?(@url)
+ return unless valid_url?(@url)
uploader = FileUploader.new(@project)
uploader.download!(@url)
diff --git a/app/services/projects/fetch_statistics_increment_service.rb b/app/services/projects/fetch_statistics_increment_service.rb
new file mode 100644
index 00000000000..8644e6bf313
--- /dev/null
+++ b/app/services/projects/fetch_statistics_increment_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Projects
+ class FetchStatisticsIncrementService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ increment_fetch_count_sql = <<~SQL
+ INSERT INTO #{table_name} (project_id, date, fetch_count)
+ VALUES (#{project.id}, '#{Date.today}', 1)
+ SQL
+
+ increment_fetch_count_sql += if Gitlab::Database.postgresql?
+ "ON CONFLICT (project_id, date) DO UPDATE SET fetch_count = #{table_name}.fetch_count + 1"
+ else
+ "ON DUPLICATE KEY UPDATE fetch_count = #{table_name}.fetch_count + 1"
+ end
+
+ ActiveRecord::Base.connection.execute(increment_fetch_count_sql)
+ end
+
+ private
+
+ def table_name
+ ProjectDailyStatistic.table_name
+ end
+ end
+end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 91091c4393d..fc234bafc57 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -38,8 +38,8 @@ module Projects
new_params = {
visibility_level: allowed_visibility_level,
description: @project.description,
- name: @project.name,
- path: @project.path,
+ name: target_name,
+ path: target_path,
shared_runners_enabled: @project.shared_runners_enabled,
namespace_id: target_namespace.id,
fork_network: fork_network,
@@ -94,6 +94,14 @@ module Projects
Projects::ForksCountService.new(@project).refresh_cache
end
+ def target_path
+ @target_path ||= @params[:path] || @project.path
+ end
+
+ def target_name
+ @target_name ||= @params[:name] || @project.name
+ end
+
def target_namespace
@target_namespace ||= @params[:namespace] || current_user.namespace
end
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
index 1392775f805..e3d5bea0852 100644
--- a/app/services/projects/group_links/create_service.rb
+++ b/app/services/projects/group_links/create_service.rb
@@ -4,13 +4,19 @@ module Projects
module GroupLinks
class CreateService < BaseService
def execute(group)
- return false unless group
+ return error('Not Found', 404) unless group && can?(current_user, :read_namespace, group)
- project.project_group_links.create(
+ link = project.project_group_links.new(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
+
+ if link.save
+ success(link: link)
+ else
+ error(link.errors.full_messages.to_sentence, 409)
+ end
end
end
end
diff --git a/app/services/projects/hashed_storage/base_attachment_service.rb b/app/services/projects/hashed_storage/base_attachment_service.rb
new file mode 100644
index 00000000000..828ab616bab
--- /dev/null
+++ b/app/services/projects/hashed_storage/base_attachment_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ AttachmentMigrationError = Class.new(StandardError)
+
+ AttachmentCannotMoveError = Class.new(StandardError)
+
+ class BaseAttachmentService < BaseService
+ # Returns the disk_path value before the execution
+ attr_reader :old_disk_path
+
+ # Returns the disk_path value after the execution
+ attr_reader :new_disk_path
+
+ # Returns the logger currently in use
+ attr_reader :logger
+
+ # Return whether this operation was skipped or not
+ #
+ # @return [Boolean] true if skipped of false otherwise
+ def skipped?
+ @skipped
+ end
+
+ protected
+
+ def move_folder!(old_path, new_path)
+ unless File.directory?(old_path)
+ logger.info("Skipped attachments move from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
+ @skipped = true
+
+ return true
+ end
+
+ if File.exist?(new_path)
+ logger.error("Cannot move attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
+ raise AttachmentCannotMoveError, "Target path '#{new_path}' already exists"
+ end
+
+ # Create base path folder on the new storage layout
+ FileUtils.mkdir_p(File.dirname(new_path))
+
+ FileUtils.mv(old_path, new_path)
+ logger.info("Project attachments moved from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")
+
+ true
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
new file mode 100644
index 00000000000..f97a28b8c3b
--- /dev/null
+++ b/app/services/projects/hashed_storage/base_repository_service.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ # Returned when repository can't be made read-only because there is already a git transfer in progress
+ RepositoryInUseError = Class.new(StandardError)
+
+ class BaseRepositoryService < BaseService
+ include Gitlab::ShellAdapter
+
+ attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki
+
+ def initialize(project, old_disk_path, logger: nil)
+ @project = project
+ @logger = logger || Gitlab::AppLogger
+ @old_disk_path = old_disk_path
+ @old_wiki_disk_path = "#{old_disk_path}.wiki"
+ @move_wiki = has_wiki?
+ end
+
+ protected
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def has_wiki?
+ gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def move_repository(from_name, to_name)
+ from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
+
+ # If we don't find the repository on either original or target we should log that as it could be an issue if the
+ # project was not originally empty.
+ if !from_exists && !to_exists
+ logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
+
+ # We return true so we still reflect the change in the database.
+ # Next time the repository is (re)created it will be under the new storage layout
+ return true
+ elsif !from_exists
+ # Repository have been moved already.
+ return true
+ end
+
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def rollback_folder_move
+ move_repository(new_disk_path, old_disk_path)
+ move_repository("#{new_disk_path}.wiki", old_wiki_disk_path)
+ end
+
+ def try_to_set_repository_read_only!
+ # Mitigate any push operation to start during migration
+ unless project.set_repository_read_only!
+ migration_error = "Target repository '#{old_disk_path}' cannot be made read-only as there is a git transfer in progress"
+ logger.error migration_error
+
+ raise RepositoryInUseError, migration_error
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index a1f0302aeb7..9eaeb6eb4e7 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -2,56 +2,37 @@
module Projects
module HashedStorage
- AttachmentMigrationError = Class.new(StandardError)
-
- class MigrateAttachmentsService < BaseService
- attr_reader :logger, :old_disk_path, :new_disk_path
-
+ class MigrateAttachmentsService < BaseAttachmentService
def initialize(project, old_disk_path, logger: nil)
@project = project
@logger = logger || Rails.logger
@old_disk_path = old_disk_path
- @new_disk_path = project.disk_path
+ @skipped = false
end
def execute
origin = FileUploader.absolute_base_dir(project)
- # It's possible that old_disk_path does not match project.disk_path. For example, that happens when we rename a project
+ # It's possible that old_disk_path does not match project.disk_path.
+ # For example, that happens when we rename a project
origin.sub!(/#{Regexp.escape(project.full_path)}\z/, old_disk_path)
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
target = FileUploader.absolute_base_dir(project)
- result = move_folder!(origin, target)
- project.save!
-
- if result && block_given?
- yield
- end
-
- result
- end
+ @new_disk_path = project.disk_path
- private
+ result = move_folder!(origin, target)
- def move_folder!(old_disk_path, new_disk_path)
- unless File.directory?(old_disk_path)
- logger.info("Skipped attachments migration from '#{old_disk_path}' to '#{new_disk_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
- return
- end
+ if result
+ project.save!
- if File.exist?(new_disk_path)
- logger.error("Cannot migrate attachments from '#{old_disk_path}' to '#{new_disk_path}', target path already exist (PROJECT_ID=#{project.id})")
- raise AttachmentMigrationError, "Target path '#{new_disk_path}' already exist"
+ yield if block_given?
+ else
+ # Rollback changes
+ project.rollback!
end
- # Create hashed storage base path folder
- FileUtils.mkdir_p(File.dirname(new_disk_path))
-
- FileUtils.mv(old_disk_path, new_disk_path)
- logger.info("Migrated project attachments from '#{old_disk_path}' to '#{new_disk_path}' (PROJECT_ID=#{project.id})")
-
- true
+ result
end
end
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index f3e026ba38c..5afa8732c0a 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -2,20 +2,10 @@
module Projects
module HashedStorage
- class MigrateRepositoryService < BaseService
- include Gitlab::ShellAdapter
-
- attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki
-
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @logger = logger || Rails.logger
- @old_disk_path = old_disk_path
- @old_wiki_disk_path = "#{old_disk_path}.wiki"
- @move_wiki = has_wiki?
- end
-
+ class MigrateRepositoryService < BaseRepositoryService
def execute
+ try_to_set_repository_read_only!
+
@old_storage_version = project.storage_version
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
project.ensure_storage_path_exists
@@ -25,7 +15,7 @@ module Projects
result = move_repository(old_disk_path, new_disk_path)
if move_wiki
- result &&= move_repository("#{old_wiki_disk_path}", "#{new_disk_path}.wiki")
+ result &&= move_repository(old_wiki_disk_path, "#{new_disk_path}.wiki")
end
if result
@@ -45,38 +35,6 @@ module Projects
result
end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def has_wiki?
- gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
-
- # If we don't find the repository on either original or target we should log that as it could be an issue if the
- # project was not originally empty.
- if !from_exists && !to_exists
- logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
- return false
- elsif !from_exists
- # Repository have been moved already.
- return true
- end
-
- gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def rollback_folder_move
- move_repository(new_disk_path, old_disk_path)
- move_repository("#{new_disk_path}.wiki", old_wiki_disk_path)
- end
end
end
end
diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb
new file mode 100644
index 00000000000..f132dca61c9
--- /dev/null
+++ b/app/services/projects/hashed_storage/migration_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ class MigrationService < BaseService
+ attr_reader :logger, :old_disk_path
+
+ def initialize(project, old_disk_path, logger: nil)
+ @project = project
+ @old_disk_path = old_disk_path
+ @logger = logger || Gitlab::AppLogger
+ end
+
+ def execute
+ # Migrate repository from Legacy to Hashed Storage
+ unless project.hashed_storage?(:repository)
+ return false unless migrate_repository
+ end
+
+ # Migrate attachments from Legacy to Hashed Storage
+ unless project.hashed_storage?(:attachments)
+ return false unless migrate_attachments
+ end
+
+ true
+ end
+
+ private
+
+ def migrate_repository
+ HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute
+ end
+
+ def migrate_attachments
+ HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/rollback_attachments_service.rb b/app/services/projects/hashed_storage/rollback_attachments_service.rb
new file mode 100644
index 00000000000..6c370ac47e9
--- /dev/null
+++ b/app/services/projects/hashed_storage/rollback_attachments_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ class RollbackAttachmentsService < BaseAttachmentService
+ def initialize(project, logger: nil)
+ @project = project
+ @logger = logger || Rails.logger
+ @old_disk_path = project.disk_path
+ end
+
+ def execute
+ origin = FileUploader.absolute_base_dir(project)
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+ target = FileUploader.absolute_base_dir(project)
+
+ @new_disk_path = FileUploader.base_dir(project)
+
+ result = move_folder!(origin, target)
+
+ if result
+ project.save!
+
+ yield if block_given?
+ else
+ # Rollback changes
+ project.rollback!
+ end
+
+ result
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb
new file mode 100644
index 00000000000..b5c971c70a5
--- /dev/null
+++ b/app/services/projects/hashed_storage/rollback_repository_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ class RollbackRepositoryService < BaseRepositoryService
+ def execute
+ try_to_set_repository_read_only!
+
+ @old_storage_version = project.storage_version
+ project.storage_version = nil
+ project.ensure_storage_path_exists
+
+ @new_disk_path = project.disk_path
+
+ result = move_repository(old_disk_path, new_disk_path)
+
+ if move_wiki
+ result &&= move_repository(old_wiki_disk_path, "#{new_disk_path}.wiki")
+ end
+
+ if result
+ project.write_repository_config
+ project.track_project_repository
+ else
+ rollback_folder_move
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+ end
+
+ project.repository_read_only = false
+ project.save!
+
+ if result && block_given?
+ yield
+ end
+
+ result
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/rollback_service.rb b/app/services/projects/hashed_storage/rollback_service.rb
new file mode 100644
index 00000000000..25767f5de5e
--- /dev/null
+++ b/app/services/projects/hashed_storage/rollback_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ class RollbackService < BaseService
+ attr_reader :logger, :old_disk_path
+
+ def initialize(project, old_disk_path, logger: nil)
+ @project = project
+ @old_disk_path = old_disk_path
+ @logger = logger || Rails.logger
+ end
+
+ def execute
+ # Rollback attachments from Hashed Storage to Legacy
+ if project.hashed_storage?(:attachments)
+ return false unless rollback_attachments
+ end
+
+ # Rollback repository from Hashed Storage to Legacy
+ if project.hashed_storage?(:repository)
+ rollback_repository
+ end
+ end
+
+ private
+
+ def rollback_attachments
+ HashedStorage::RollbackAttachmentsService.new(project, logger: logger).execute
+ end
+
+ def rollback_repository
+ HashedStorage::RollbackRepositoryService.new(project, old_disk_path, logger: logger).execute
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
deleted file mode 100644
index a0e734005f8..00000000000
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class HashedStorageMigrationService < BaseService
- attr_reader :logger, :old_disk_path
-
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @old_disk_path = old_disk_path
- @logger = logger || Rails.logger
- end
-
- def execute
- # Migrate repository from Legacy to Hashed Storage
- unless project.hashed_storage?(:repository)
- return unless HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute
- end
-
- # Migrate attachments from Legacy to Hashed Storage
- unless project.hashed_storage?(:attachments)
- HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute
- end
-
- true
- end
- end
-end
diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb
new file mode 100644
index 00000000000..a0fc5149bb4
--- /dev/null
+++ b/app/services/projects/import_error_filter.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Projects
+ # Used by project imports, it removes any potential paths
+ # included in an error message that could be stored in the DB
+ class ImportErrorFilter
+ ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
+ FILTER_MESSAGE = '[FILTERED]'
+
+ def self.filter_message(message)
+ message.gsub(ERROR_MESSAGE_FILTER, FILTER_MESSAGE)
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 0c426faa22d..7214e9efaf6 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -24,8 +24,16 @@ module Projects
import_data
success
- rescue => e
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}")
+ rescue => e
+ message = Projects::ImportErrorFilter.filter_message(e.message)
+
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
+ error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}")
end
private
@@ -35,7 +43,7 @@ module Projects
begin
Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Error, "Blocked import URL: #{e.message}"
+ raise e, "Blocked import URL: #{e.message}"
end
end
@@ -65,7 +73,7 @@ module Projects
project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else
- gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url)
+ gitlab_shell.import_project_repository(project)
end
rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as:
@@ -86,11 +94,11 @@ module Projects
return unless project.lfs_enabled?
- oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- download_service = Projects::LfsPointers::LfsDownloadService.new(project)
+ lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- oids_to_download.each do |oid, link|
- download_service.execute(oid, link)
+ lfs_objects_to_download.each do |lfs_download_object|
+ Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
+ .execute
end
rescue => e
# Right now, to avoid aborting the importing process, we silently fail
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index a837ea82e38..7998976b00a 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -41,16 +41,17 @@ module Projects
end
def parse_response_links(objects_response)
- objects_response.each_with_object({}) do |entry, link_list|
+ objects_response.each_with_object([]) do |entry, link_list|
begin
- oid = entry['oid']
link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
raise DownloadLinkNotFound unless link
- link_list[oid] = add_credentials(link)
- rescue DownloadLinkNotFound, URI::InvalidURIError
- Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
+ link_list << LfsDownloadObject.new(oid: entry['oid'],
+ size: entry['size'],
+ link: add_credentials(link))
+ rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
+ log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
end
end
end
@@ -70,7 +71,7 @@ module Projects
end
def add_credentials(link)
- uri = URI.parse(link)
+ uri = Addressable::URI.parse(link)
if should_add_credentials?(uri)
uri.user = remote_uri.user
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index f9b9781ad5f..398f00a598d 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,49 +4,93 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
- VALID_PROTOCOLS = %w[http https].freeze
+ SizeError = Class.new(StandardError)
+ OidError = Class.new(StandardError)
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(oid, url)
- return unless project&.lfs_enabled? && oid.present? && url.present?
+ attr_reader :lfs_download_object
+ delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
- return if LfsObject.exists?(oid: oid)
+ def initialize(project, lfs_download_object)
+ super(project)
- sanitized_uri = Gitlab::UrlSanitizer.new(url)
- Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url, protocols: VALID_PROTOCOLS)
+ @lfs_download_object = lfs_download_object
+ end
- with_tmp_file(oid) do |file|
- size = download_and_save_file(file, sanitized_uri)
- lfs_object = LfsObject.new(oid: oid, size: size, file: file)
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ return unless project&.lfs_enabled? && lfs_download_object
+ return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
+ return if LfsObject.exists?(oid: lfs_oid)
- project.all_lfs_objects << lfs_object
+ wrap_download_errors do
+ download_lfs_file!
end
- rescue StandardError => e
- Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
# rubocop: enable CodeReuse/ActiveRecord
private
- def download_and_save_file(file, sanitized_uri)
- IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
+ def wrap_download_errors(&block)
+ yield
+ rescue SizeError, OidError, StandardError => e
+ error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}")
+ end
+
+ def download_lfs_file!
+ with_tmp_file do |tmp_file|
+ download_and_save_file!(tmp_file)
+ project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
+ size: lfs_size,
+ file: tmp_file)
+
+ success
+ end
end
- def headers(sanitized_uri)
- {}.tap do |headers|
- credentials = sanitized_uri.credentials
+ def download_and_save_file!(file)
+ digester = Digest::SHA256.new
+ response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
+ digester << fragment
+ file.write(fragment)
+
+ raise_size_error! if file.size > lfs_size
+ end
+
+ raise StandardError, "Received error code #{response.code}" unless response.success?
+
+ raise_size_error! if file.size != lfs_size
+ raise_oid_error! if digester.hexdigest != lfs_oid
+ end
- if credentials[:user].present? || credentials[:password].present?
+ def download_headers
+ { stream_body: true }.tap do |headers|
+ if lfs_credentials[:user].present? || lfs_credentials[:password].present?
# Using authentication headers in the request
- headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
+ headers[:basic_auth] = { username: lfs_credentials[:user], password: lfs_credentials[:password] }
end
end
end
- def with_tmp_file(oid)
+ def with_tmp_file
create_tmp_storage_dir
- File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
+ File.open(tmp_filename, 'wb') do |file|
+ begin
+ yield file
+ rescue StandardError => e
+ # If the lfs file is successfully downloaded it will be removed
+ # when it is added to the project's lfs files.
+ # Nevertheless if any excetion raises the file would remain
+ # in the file system. Here we ensure to remove it
+ File.unlink(file) if File.exist?(file)
+
+ raise e
+ end
+ end
+ end
+
+ def tmp_filename
+ File.join(tmp_storage_dir, lfs_oid)
end
def create_tmp_storage_dir
@@ -60,6 +104,20 @@ module Projects
def storage_dir
@storage_dir ||= Gitlab.config.lfs.storage_path
end
+
+ def raise_size_error!
+ raise SizeError, 'Size mistmatch'
+ end
+
+ def raise_oid_error!
+ raise OidError, 'Oid mismatch'
+ end
+
+ def error(message, http_status = nil)
+ log_error(message)
+
+ super
+ end
end
end
end
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
new file mode 100644
index 00000000000..aedf79c86d7
--- /dev/null
+++ b/app/services/projects/operations/update_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Projects
+ module Operations
+ class UpdateService < BaseService
+ def execute
+ Projects::UpdateService
+ .new(project, current_user, project_update_params)
+ .execute
+ end
+
+ private
+
+ def project_update_params
+ error_tracking_params
+ end
+
+ def error_tracking_params
+ settings = params[:error_tracking_setting_attributes]
+ return {} if settings.blank?
+
+ api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
+ api_host: settings[:api_host],
+ project_slug: settings.dig(:project, :slug),
+ organization_slug: settings.dig(:project, :organization_slug)
+ )
+
+ {
+ error_tracking_setting_attributes: {
+ api_url: api_url,
+ token: settings[:token],
+ enabled: settings[:enabled],
+ project_name: settings.dig(:project, :name),
+ organization_name: settings.dig(:project, :organization_name)
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/app/services/projects/protect_default_branch_service.rb b/app/services/projects/protect_default_branch_service.rb
new file mode 100644
index 00000000000..245490791bf
--- /dev/null
+++ b/app/services/projects/protect_default_branch_service.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class that can be used to execute actions necessary after creating a
+ # default branch.
+ class ProtectDefaultBranchService
+ attr_reader :project, :default_branch_protection
+
+ # @param [Project] project
+ def initialize(project)
+ @project = project
+
+ @default_branch_protection = Gitlab::Access::BranchProtection
+ .new(Gitlab::CurrentSettings.default_branch_protection)
+ end
+
+ def execute
+ protect_default_branch if default_branch
+ end
+
+ def protect_default_branch
+ # Ensure HEAD points to the default branch in case it is not master
+ project.change_head(default_branch)
+
+ create_protected_branch if protect_branch?
+ end
+
+ def create_protected_branch
+ params = {
+ name: default_branch,
+ push_access_levels_attributes: [{ access_level: push_access_level }],
+ merge_access_levels_attributes: [{ access_level: merge_access_level }]
+ }
+
+ # The creator of the project is always allowed to create protected
+ # branches, so we skip the authorization check in this service class.
+ ProtectedBranches::CreateService
+ .new(project, project.creator, params)
+ .execute(skip_authorization: true)
+ end
+
+ def protect_branch?
+ default_branch_protection.any? &&
+ !ProtectedBranch.protected?(project, default_branch)
+ end
+
+ def default_branch
+ project.default_branch
+ end
+
+ def push_access_level
+ if default_branch_protection.developer_can_push?
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MAINTAINER
+ end
+ end
+
+ def merge_access_level
+ if default_branch_protection.developer_can_merge?
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MAINTAINER
+ end
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index abf40b3ad7a..674071ad92a 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -2,6 +2,8 @@
module Projects
class UpdatePagesConfigurationService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :project
def initialize(project)
@@ -9,15 +11,25 @@ module Projects
end
def execute
- update_file(pages_config_file, pages_config.to_json)
+ if file_equals?(pages_config_file, pages_config_json)
+ return success(reload: false)
+ end
+
+ update_file(pages_config_file, pages_config_json)
reload_daemon
- success
+ success(reload: true)
rescue => e
error(e.message)
end
private
+ def pages_config_json
+ strong_memoize(:pages_config_json) do
+ pages_config.to_json
+ end
+ end
+
def pages_config
{
domains: pages_domains_config,
@@ -67,11 +79,6 @@ module Projects
end
def update_file(file, data)
- unless data
- FileUtils.remove(file, force: true)
- return
- end
-
temp_file = "#{file}.#{SecureRandom.hex(16)}"
File.open(temp_file, 'w') do |f|
f.write(data)
@@ -81,5 +88,18 @@ module Projects
# In case if the updating fails
FileUtils.remove(temp_file, force: true)
end
+
+ def file_equals?(file, data)
+ existing_data = read_file(file)
+ data == existing_data.to_s
+ end
+
+ def read_file(file)
+ File.open(file, 'r') do |f|
+ f.read
+ end
+ rescue
+ nil
+ end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index eb2478be3cf..5caeb4cfa5f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -7,7 +7,11 @@ module Projects
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
- SITE_PATH = 'public/'.freeze
+ PUBLIC_DIR = 'public'.freeze
+
+ # this has to be invalid group name,
+ # as it shares the namespace with groups
+ TMP_EXTRACT_PATH = '@pages.tmp'.freeze
attr_reader :build
@@ -27,12 +31,11 @@ module Projects
raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
- FileUtils.mkdir_p(tmp_path)
- Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ make_secure_tmp_dir(tmp_path) do |archive_path|
extract_archive!(archive_path)
# Check if we did extract public directory
- archive_public_path = File.join(archive_path, 'public')
+ archive_public_path = File.join(archive_path, PUBLIC_DIR)
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvalidStateError, 'pages are outdated' unless latest?
@@ -85,22 +88,18 @@ module Projects
raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
- public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+ public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true)
if public_entry.total_size > max_size
raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
- # Requires UnZip at least 6.00 Info-ZIP.
- # -qq be (very) quiet
- # -n never overwrite existing files
- # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
- site_path = File.join(SITE_PATH, '*')
build.artifacts_file.use_file do |artifacts_path|
- unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
- raise FailedToExtractError, 'pages failed to extract'
- end
+ SafeZip::Extract.new(artifacts_path)
+ .extract(directories: [PUBLIC_DIR], to: temp_path)
end
+ rescue SafeZip::Extract::Error => e
+ raise FailedToExtractError, e.message
end
def deploy_page!(archive_public_path)
@@ -139,7 +138,7 @@ module Projects
end
def tmp_path
- @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ @tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH)
end
def pages_path
@@ -147,11 +146,11 @@ module Projects
end
def public_path
- @public_path ||= File.join(pages_path, 'public')
+ @public_path ||= File.join(pages_path, PUBLIC_DIR)
end
def previous_public_path
- @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ @previous_public_path ||= File.join(pages_path, "#{PUBLIC_DIR}.#{SecureRandom.hex}")
end
def ref
@@ -188,5 +187,15 @@ module Projects
def pages_deployments_failed_total_counter
@pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
end
+
+ def make_secure_tmp_dir(tmp_path)
+ FileUtils.mkdir_p(tmp_path)
+ path = Dir.mktmpdir(nil, tmp_path)
+ begin
+ yield(path)
+ ensure
+ FileUtils.remove_entry_secure(path)
+ end
+ end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 93e48fc0199..6856009b395 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -61,13 +61,13 @@ module Projects
if project.previous_changes.include?(:visibility_level) && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::ProjectPrivateWorker.perform_in(1.hour, project.id)
+ TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
elsif (project_changed_feature_keys & todos_features_changes).present?
- TodosDestroyer::PrivateFeaturesWorker.perform_in(1.hour, project.id)
+ TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
end
if project.previous_changes.include?('path')
- AfterRenameService.new(project).execute
+ after_rename_service(project).execute
else
system_hook_service.execute_hooks_for(project, :update)
end
@@ -75,6 +75,13 @@ module Projects
update_pages_config if changing_pages_related_config?
end
+ def after_rename_service(project)
+ # The path slug the project was using, before the rename took place.
+ path_before = project.previous_changes['path'].first
+
+ AfterRenameService.new(project, path_before: path_before, full_path_before: project.full_path_was)
+ end
+
def changing_pages_related_config?
changing_pages_https_only? || changing_pages_access_level?
end
diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb
index a791845ba20..3be958e1613 100644
--- a/app/services/prometheus/adapter_service.rb
+++ b/app/services/prometheus/adapter_service.rb
@@ -30,7 +30,7 @@ module Prometheus
return unless deployment_platform.respond_to?(:cluster)
cluster = deployment_platform.cluster
- return unless cluster.application_prometheus&.ready?
+ return unless cluster.application_prometheus&.available?
cluster.application_prometheus
end
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index 4340d3e8260..1b13dace5f2 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -3,24 +3,15 @@
module ProtectedBranches
class ApiService < BaseService
def create
- @push_params = AccessLevelParams.new(:push, params)
- @merge_params = AccessLevelParams.new(:merge, params)
-
- verify_params!
-
- protected_branch_params = {
- name: params[:name],
- push_access_levels_attributes: @push_params.access_levels,
- merge_access_levels_attributes: @merge_params.access_levels
- }
-
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
- private
-
- def verify_params!
- # EE-only
+ def protected_branch_params
+ {
+ name: params[:name],
+ push_access_levels_attributes: AccessLevelParams.new(:push, params).access_levels,
+ merge_access_levels_attributes: AccessLevelParams.new(:merge, params).access_levels
+ }
end
end
end
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index da8bf2ce02a..7cb8d41818f 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -6,30 +6,31 @@
# lives in this service.
module ProtectedBranches
class LegacyApiUpdateService < BaseService
+ attr_reader :protected_branch, :developers_can_push, :developers_can_merge
+
def execute(protected_branch)
+ @protected_branch = protected_branch
@developers_can_push = params.delete(:developers_can_push)
@developers_can_merge = params.delete(:developers_can_merge)
- @protected_branch = protected_branch
-
protected_branch.transaction do
delete_redundant_access_levels
- case @developers_can_push
+ case developers_can_push
when true
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
end
- case @developers_can_merge
+ case developers_can_merge
when true
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
when false
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
end
- service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
+ service = ProtectedBranches::UpdateService.new(project, current_user, params)
service.execute(protected_branch)
end
end
@@ -37,12 +38,12 @@ module ProtectedBranches
private
def delete_redundant_access_levels
- unless @developers_can_merge.nil?
- @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll
+ unless developers_can_merge.nil?
+ protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll
end
- unless @developers_can_push.nil?
- @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll
+ unless developers_can_push.nil?
+ protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll
end
end
end
diff --git a/app/services/push_event_payload_service.rb b/app/services/push_event_payload_service.rb
index bb1259787af..fe366ac225b 100644
--- a/app/services/push_event_payload_service.rb
+++ b/app/services/push_event_payload_service.rb
@@ -46,7 +46,7 @@ class PushEventPayloadService
def commit_title
commit = @push_data.fetch(:commits).last
- return nil unless commit && commit[:message]
+ return unless commit && commit[:message]
raw_msg = commit[:message]
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index d248b10f41e..131efb8925e 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -2,10 +2,16 @@
module QuickActions
class InterpretService < BaseService
+ include Gitlab::Utils::StrongMemoize
include Gitlab::QuickActions::Dsl
attr_reader :issuable
+ # Counts how many commands have been executed.
+ # Used to display relevant feedback on UI when a note
+ # with only commands has been processed.
+ attr_accessor :commands_executed_count
+
SHRUG = '¯\\_(ツ)_/¯'.freeze
TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze
@@ -210,15 +216,9 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- if project
- available_labels = LabelsFinder
- .new(current_user, project_id: project.id, include_ancestor_groups: true)
- .execute
- end
-
- project &&
- current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
- available_labels.any?
+ parent &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", parent) &&
+ find_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
@@ -245,7 +245,7 @@ module QuickActions
issuable.is_a?(Issuable) &&
issuable.persisted? &&
issuable.labels.any? &&
- current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", parent)
end
command :unlabel do |labels_param = nil|
if labels_param.present?
@@ -674,9 +674,25 @@ module QuickActions
MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute
end
- def find_labels(labels_param)
- extract_references(labels_param, :label) |
- LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
+ def parent
+ project || group
+ end
+
+ def group
+ strong_memoize(:group) do
+ issuable.group if issuable.respond_to?(:group)
+ end
+ end
+
+ def find_labels(labels_params = nil)
+ finder_params = { include_ancestor_groups: true }
+ finder_params[:project_id] = project.id if project
+ finder_params[:group_id] = group.id if group
+ finder_params[:name] = labels_params.split if labels_params
+
+ result = LabelsFinder.new(current_user, finder_params).execute
+
+ extract_references(labels_params, :label) | result
end
def find_label_references(labels_param)
@@ -707,9 +723,11 @@ module QuickActions
# rubocop: disable CodeReuse/ActiveRecord
def extract_references(arg, type)
+ return [] unless arg
+
ext = Gitlab::ReferenceExtractor.new(project, current_user)
- ext.analyze(arg, author: current_user)
+ ext.analyze(arg, author: current_user, group: group)
ext.references(type)
end
diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb
new file mode 100644
index 00000000000..a04bb8f9e14
--- /dev/null
+++ b/app/services/releases/concerns.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Releases
+ module Concerns
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ included do
+ def tag_name
+ params[:tag]
+ end
+
+ def ref
+ params[:ref]
+ end
+
+ def name
+ params[:name]
+ end
+
+ def description
+ params[:description]
+ end
+
+ def release
+ strong_memoize(:release) do
+ project.releases.find_by_tag(tag_name)
+ end
+ end
+
+ def existing_tag
+ strong_memoize(:existing_tag) do
+ repository.find_tag(tag_name)
+ end
+ end
+
+ def tag_exist?
+ existing_tag.present?
+ end
+
+ def repository
+ strong_memoize(:repository) do
+ project.repository
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
new file mode 100644
index 00000000000..c6e143d440d
--- /dev/null
+++ b/app/services/releases/create_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Releases
+ class CreateService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Access Denied', 403) unless allowed?
+ return error('Release already exists', 409) if release
+
+ tag = ensure_tag
+
+ return tag unless tag.is_a?(Gitlab::Git::Tag)
+
+ create_release(tag)
+ end
+
+ private
+
+ def ensure_tag
+ existing_tag || create_tag
+ end
+
+ def create_tag
+ return error('Ref is not specified', 422) unless ref
+
+ result = Tags::CreateService
+ .new(project, current_user)
+ .execute(tag_name, ref, nil)
+
+ return result unless result[:status] == :success
+
+ result[:tag]
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :create_release, project)
+ end
+
+ def create_release(tag)
+ release = project.releases.create!(
+ name: name,
+ description: description,
+ author: current_user,
+ tag: tag.name,
+ sha: tag.dereferenced_target.sha,
+ links_attributes: params.dig(:assets, 'links') || []
+ )
+
+ success(tag: tag, release: release)
+ rescue => e
+ error(e.message, 400)
+ end
+ end
+end
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
new file mode 100644
index 00000000000..8c2bc3b4e6e
--- /dev/null
+++ b/app/services/releases/destroy_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Releases
+ class DestroyService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Tag does not exist', 404) unless existing_tag
+ return error('Release does not exist', 404) unless release
+ return error('Access Denied', 403) unless allowed?
+
+ if release.destroy
+ success(tag: existing_tag, release: release)
+ else
+ error(release.errors.messages || '400 Bad request', 400)
+ end
+ end
+
+ private
+
+ def allowed?
+ Ability.allowed?(current_user, :destroy_release, release)
+ end
+ end
+end
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
new file mode 100644
index 00000000000..fabfa398c59
--- /dev/null
+++ b/app/services/releases/update_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Releases
+ class UpdateService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Tag does not exist', 404) unless existing_tag
+ return error('Release does not exist', 404) unless release
+ return error('Access Denied', 403) unless allowed?
+ return error('params is empty', 400) if empty_params?
+
+ if release.update(params)
+ success(tag: existing_tag, release: release)
+ else
+ error(release.errors.messages || '400 Bad request', 400)
+ end
+ end
+
+ private
+
+ def allowed?
+ Ability.allowed?(current_user, :update_release, release)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def empty_params?
+ params.except(:tag).empty?
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index cb1bf0a03a5..d6af26d949d 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -2,6 +2,8 @@
module Search
class GlobalService
+ include Gitlab::Utils::StrongMemoize
+
attr_accessor :current_user, :params
attr_reader :default_project_filter
@@ -19,11 +21,15 @@ module Search
@projects ||= ProjectsFinder.new(current_user: current_user).execute
end
- def scope
- @scope ||= begin
- allowed_scopes = %w[issues merge_requests milestones]
+ def allowed_scopes
+ strong_memoize(:allowed_scopes) do
+ %w[issues merge_requests milestones]
+ end
+ end
- allowed_scopes.delete(params[:scope]) { 'projects' }
+ def scope
+ strong_memoize(:scope) do
+ allowed_scopes.include?(params[:scope]) ? params[:scope] : 'projects'
end
end
end
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
index d931d528c86..f778c5aa5f5 100644
--- a/app/services/suggestions/apply_service.rb
+++ b/app/services/suggestions/apply_service.rb
@@ -11,7 +11,17 @@ module Suggestions
return error('Suggestion is not appliable')
end
- params = file_update_params(suggestion)
+ unless latest_source_head?(suggestion)
+ return error('The file has been changed')
+ end
+
+ diff_file = suggestion.note.latest_diff_file
+
+ unless diff_file
+ return error('The file was not found')
+ end
+
+ params = file_update_params(suggestion, diff_file)
result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute
if result[:status] == :success
@@ -19,30 +29,45 @@ module Suggestions
end
result
+ rescue Files::UpdateService::FileChangedError
+ error('The file has been changed')
end
private
- def file_update_params(suggestion)
- diff_file = suggestion.diff_file
+ # Checks whether the latest source branch HEAD matches with
+ # the position HEAD we're using to update the file content. Since
+ # the persisted HEAD is updated async (for MergeRequest),
+ # it's more consistent to fetch this data directly from the
+ # repository.
+ def latest_source_head?(suggestion)
+ suggestion.position.head_sha == suggestion.noteable.source_branch_sha
+ end
- file_path = diff_file.file_path
- branch_name = suggestion.noteable.source_branch
- file_content = new_file_content(suggestion)
+ def file_update_params(suggestion, diff_file)
+ blob = diff_file.new_blob
+ file_path = suggestion.file_path
+ branch_name = suggestion.branch
+ file_content = new_file_content(suggestion, blob)
commit_message = "Apply suggestion to #{file_path}"
+ file_last_commit =
+ Gitlab::Git::Commit.last_for_path(suggestion.project.repository,
+ blob.commit_id,
+ blob.path)
+
{
file_path: file_path,
branch_name: branch_name,
start_branch: branch_name,
commit_message: commit_message,
- file_content: file_content
+ file_content: file_content,
+ last_commit_sha: file_last_commit&.id
}
end
- def new_file_content(suggestion)
+ def new_file_content(suggestion, blob)
range = suggestion.from_line_index..suggestion.to_line_index
- blob = suggestion.diff_file.new_blob
blob.load_all_data!
content = blob.data.lines
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index 77e958cbe0c..c7ac2452c53 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -9,6 +9,10 @@ module Suggestions
def execute
return unless @note.supports_suggestion?
+ diff_file = @note.latest_diff_file
+
+ return unless diff_file
+
suggestions = Banzai::SuggestionsParser.parse(@note.note)
# For single line suggestion we're only looking forward to
@@ -20,7 +24,7 @@ module Suggestions
rows =
suggestions.map.with_index do |suggestion, index|
- from_content = changing_lines(comment_line, comment_line)
+ from_content = changing_lines(diff_file, comment_line, comment_line)
# The parsed suggestion doesn't have information about the correct
# ending characters (we may have a line break, or not), so we take
@@ -44,8 +48,8 @@ module Suggestions
private
- def changing_lines(from_line, to_line)
- @note.diff_file.new_blob_lines_between(from_line, to_line).join
+ def changing_lines(diff_file, from_line, to_line)
+ diff_file.new_blob_lines_between(from_line, to_line).join
end
def line_break_chars(line)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index ec6c306227b..ea8ac7e4656 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -360,7 +360,7 @@ module SystemNoteService
# author - User performing the change
# branch_type - 'source' or 'target'
# old_branch - old branch name
- # new_branch - new branch nmae
+ # new_branch - new branch name
#
# Example Note text:
#
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index 6bb9bb3988e..4de6b2d2774 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -2,7 +2,7 @@
module Tags
class CreateService < BaseService
- def execute(tag_name, target, message, release_description = nil)
+ def execute(tag_name, target, message)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag
@@ -20,10 +20,7 @@ module Tags
end
if new_tag
- if release_description.present?
- CreateReleaseService.new(@project, @current_user)
- .execute(tag_name, release_description)
- end
+ repository.expire_tags_cache
success.merge(tag: new_tag)
else
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 6bfef09ac54..cab507946b4 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -2,7 +2,6 @@
module Tags
class DestroyService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
@@ -12,8 +11,12 @@ module Tags
end
if repository.rm_tag(current_user, tag_name)
- release = project.releases.find_by(tag: tag_name)
- release&.destroy
+ ##
+ # When a tag in a repository is destroyed,
+ # release assets will be destroyed too.
+ Releases::DestroyService
+ .new(project, current_user, tag: tag_name)
+ .execute
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
@@ -27,7 +30,6 @@ module Tags
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
- # rubocop: enable CodeReuse/ActiveRecord
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb
new file mode 100644
index 00000000000..f6602a35033
--- /dev/null
+++ b/app/services/task_list_toggle_service.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+# Finds the correct checkbox in the passed in markdown/html and toggles it's state,
+# returning the updated markdown/html.
+# We don't care if the text has changed above or below the specific checkbox, as long
+# the checkbox still exists at exactly the same line number and the text is equal.
+# If successful, new values are available in `updated_markdown` and `updated_markdown_html`
+class TaskListToggleService
+ attr_reader :updated_markdown, :updated_markdown_html
+
+ def initialize(markdown, markdown_html, line_source:, line_number:, toggle_as_checked:)
+ @markdown, @markdown_html = markdown, markdown_html
+ @line_source, @line_number = line_source, line_number
+ @toggle_as_checked = toggle_as_checked
+
+ @updated_markdown, @updated_markdown_html = nil
+ end
+
+ def execute
+ return false unless markdown && markdown_html
+
+ toggle_markdown && toggle_markdown_html
+ end
+
+ private
+
+ attr_reader :markdown, :markdown_html, :toggle_as_checked
+ attr_reader :line_source, :line_number
+
+ def toggle_markdown
+ source_lines = markdown.split("\n")
+ source_line_index = line_number - 1
+ markdown_task = source_lines[source_line_index]
+
+ # The source in the DB could be using either \n or \r\n line endings
+ return unless markdown_task.chomp == line_source
+ return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task)
+
+ currently_checked = TaskList::Item.new(source_checkbox[1]).complete?
+
+ # Check `toggle_as_checked` to make sure we don't accidentally replace
+ # any `[ ]` or `[x]` in the middle of the text
+ if currently_checked
+ markdown_task.sub!(Taskable::COMPLETE_PATTERN, '[ ]') unless toggle_as_checked
+ else
+ markdown_task.sub!(Taskable::INCOMPLETE_PATTERN, '[x]') if toggle_as_checked
+ end
+
+ source_lines[source_line_index] = markdown_task
+ @updated_markdown = source_lines.join("\n")
+ end
+
+ def toggle_markdown_html
+ html = Nokogiri::HTML.fragment(markdown_html)
+ html_checkbox = get_html_checkbox(html)
+ return unless html_checkbox
+
+ if toggle_as_checked
+ html_checkbox[:checked] = 'checked'
+ else
+ html_checkbox.remove_attribute('checked')
+ end
+
+ @updated_markdown_html = html.to_html
+ end
+
+ # When using CommonMark, we should be able to use the embedded `sourcepos` attribute to
+ # target the exact line in the DOM.
+ def get_html_checkbox(html)
+ html.css(".task-list-item[data-sourcepos^='#{line_number}:'] input.task-list-item-checkbox").first
+ end
+end
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
deleted file mode 100644
index e2228ca026c..00000000000
--- a/app/services/update_release_service.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateReleaseService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(tag_name, release_description)
- repository = project.repository
- existing_tag = repository.find_tag(tag_name)
-
- if existing_tag
- release = project.releases.find_by(tag: tag_name)
-
- if release
- release.update(description: release_description)
-
- success(release)
- else
- error('Release does not exist', 404)
- end
- else
- error('Tag does not exist', 404)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def success(release)
- super().merge(release: release)
- end
-end
diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb
index 39909ee4f82..403944557a2 100644
--- a/app/services/upload_service.rb
+++ b/app/services/upload_service.rb
@@ -6,12 +6,12 @@ class UploadService
end
def execute
- return nil unless @file && @file.size <= max_attachment_size
+ return unless @file && @file.size <= max_attachment_size
uploader = @uploader_class.new(@model, nil, @uploader_context)
uploader.store!(@file)
- uploader.to_h
+ uploader
end
private
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index db03ba8756f..e50840a9158 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -26,12 +26,15 @@ module Users
def record_activity
return if Gitlab::Database.read_only?
+ today = Date.today
+
+ return if @user.last_activity_on == today
+
lease = Gitlab::ExclusiveLease.new("acitvity_service:#{@user.id}",
timeout: LEASE_TIMEOUT)
return unless lease.try_obtain
- @user.update_attribute(:last_activity_on, Date.today)
- Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@user.id} (username: #{@user.username})")
+ @user.update_attribute(:last_activity_on, today)
end
end
end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index af4fe1aebb9..0b00bd135eb 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -55,7 +55,7 @@ module Users
params.reject! { |key, _| read_only.include?(key.to_sym) }
end
- @user.assign_attributes(params) if params.any?
+ @user.assign_attributes(params) unless params.empty?
end
end
end
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index c19e2ec2043..3f4a59e5cee 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -3,14 +3,14 @@
require_relative 'base_service'
class ValidateNewBranchService < BaseService
- def execute(branch_name)
+ def execute(branch_name, force: false)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
unless valid_branch
return error('Branch name is invalid')
end
- if project.repository.branch_exists?(branch_name)
+ if project.repository.branch_exists?(branch_name) && !force
return error('Branch already exists')
end
diff --git a/app/uploaders/external_diff_uploader.rb b/app/uploaders/external_diff_uploader.rb
new file mode 100644
index 00000000000..d2707cd0777
--- /dev/null
+++ b/app/uploaders/external_diff_uploader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class ExternalDiffUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.external_diffs
+
+ alias_method :upload, :model
+
+ def filename
+ "diff-#{model.id}"
+ end
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ File.join(model.model_name.plural, "mr-#{model.merge_request_id}")
+ end
+end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index a7f8615e9ba..236b7ed2b3d 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -11,6 +11,8 @@ class FileMover
end
def execute
+ return unless valid?
+
move
if update_markdown
@@ -21,6 +23,12 @@ class FileMover
private
+ def valid?
+ Pathname.new(temp_file_path).realpath.to_path.start_with?(
+ (Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
+ )
+ end
+
def move
FileUtils.mkdir_p(File.dirname(file_path))
FileUtils.move(temp_file_path, file_path)
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index 25474b494ff..272837aa6ce 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -6,8 +6,15 @@ class PersonalFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model, _store = nil)
- File.join(options.base_dir, model_path_segment(model))
+ def self.base_dir(model, store = nil)
+ base_dirs(model)[store || Store::LOCAL]
+ end
+
+ def self.base_dirs(model)
+ {
+ Store::LOCAL => File.join(options.base_dir, model_path_segment(model)),
+ Store::REMOTE => model_path_segment(model)
+ }
end
def self.model_path_segment(model)
@@ -33,13 +40,6 @@ class PersonalFileUploader < FileUploader
store_dirs[object_store]
end
- def store_dirs
- {
- Store::LOCAL => File.join(base_dir, dynamic_segment),
- Store::REMOTE => File.join(self.class.model_path_segment(model), dynamic_segment)
- }
- end
-
private
def secure_url
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 0efca895a50..9a243e07936 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -23,13 +23,23 @@ module RecordsUploads
return unless model
return unless file && file.exists?
- Upload.transaction do
- uploads.where(path: upload_path).delete_all
- upload.delete if upload
-
- self.upload = build_upload.tap(&:save!)
+ # MySQL InnoDB may encounter a deadlock if a deletion and an
+ # insert is in the same transaction due to its next-key locking
+ # algorithm, so we need to skip the transaction.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/55161#note_131556351
+ if Gitlab::Database.mysql?
+ readd_upload
+ else
+ Upload.transaction { readd_upload }
end
end
+
+ def readd_upload
+ uploads.where(path: upload_path).delete_all
+ upload.delete if upload
+
+ self.upload = build_upload.tap(&:save!)
+ end
# rubocop: enable CodeReuse/ActiveRecord
def upload_path
diff --git a/app/validators/sha_validator.rb b/app/validators/sha_validator.rb
new file mode 100644
index 00000000000..085fca4d65d
--- /dev/null
+++ b/app/validators/sha_validator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ShaValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.blank? || value.match(/\A\h{40}\z/)
+
+ record.errors.add(attribute, 'is not a valid SHA')
+ end
+end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 5feb0b0f05b..3fd015c3cf5 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -26,6 +26,7 @@
# - allow_local_network: Allow urls pointing to private network addresses. Default: true
# - ports: Allowed ports. Default: all.
# - enforce_user: Validate user format. Default: false
+# - enforce_sanitization: Validate that there are no html/css/js tags. Default: false
#
# Example:
# class User < ActiveRecord::Base
@@ -70,7 +71,8 @@ class UrlValidator < ActiveModel::EachValidator
allow_localhost: true,
allow_local_network: true,
ascii_only: false,
- enforce_user: false
+ enforce_user: false,
+ enforce_sanitization: false
}
end
@@ -91,6 +93,12 @@ class UrlValidator < ActiveModel::EachValidator
end
def allow_setting_local_requests?
+ # We cannot use Gitlab::CurrentSettings as ApplicationSetting itself
+ # uses UrlValidator to validate urls. This ends up in a cycle
+ # when Gitlab::CurrentSettings creates an ApplicationSetting which then
+ # calls this validator.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ee/issues/9833
ApplicationSetting.current&.allow_local_requests_from_hooks_and_services?
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index cb67079853e..a5f34d0dab2 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,86 +1,97 @@
-= form_for @appearance, url: admin_appearances_path do |f|
+= form_for @appearance, url: admin_appearances_path, html: { class: 'prepend-top-default' } do |f|
= form_errors(@appearance)
- %fieldset.app_logo
- %legend
- Navigation bar:
- .form-group.row
- = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label pt-0'
- .col-sm-10
- - if @appearance.header_logo?
- = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
- %hr
- = f.hidden_field :header_logo_cache
- = f.file_field :header_logo, class: ""
- .hint
- Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
- %fieldset.app_logo
- %legend
- Favicon:
- .form-group.row
- = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label pt-0'
- .col-sm-10
- - if @appearance.favicon?
- = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
- %hr
- = f.hidden_field :favicon_cache
- = f.file_field :favicon, class: ''
- .hint
- Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
- %br
- Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
+ .row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0 Navigation bar
- %fieldset.sign-in
- %legend
- Sign in/Sign up pages:
- .form-group.row
- = f.label :title, class: 'col-sm-2 col-form-label'
- .col-sm-10
- = f.text_field :title, class: "form-control"
- .form-group.row
- = f.label :description, class: 'col-sm-2 col-form-label'
- .col-sm-10
- = f.text_area :description, class: "form-control", rows: 10
- .hint
- Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
- .form-group.row
- = f.label :logo, class: 'col-sm-2 col-form-label pt-0'
- .col-sm-10
- - if @appearance.logo?
- = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- - if @appearance.persisted?
- %br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
- %hr
- = f.hidden_field :logo_cache
- = f.file_field :logo, class: ""
- .hint
- Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+ .col-lg-8
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'col-form-label label-bold pt-0'
+ %p
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ %hr
+ .row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0 Favicon
+
+ .col-lg-8
+ .form-group
+ = f.label :favicon, 'Favicon', class: 'col-form-label label-bold pt-0'
+ %p
+ - if @appearance.favicon?
+ = image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :favicon_cache
+ = f.file_field :favicon, class: ''
+ .hint
+ Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
+ %br
+ Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
+
+ = render partial: 'admin/appearances/system_header_footer_form', locals: { form: f }
- %fieldset
- %legend
- New project pages:
- .form-group.row
- = f.label :new_project_guidelines, class: 'col-sm-2 col-form-label'
- .col-sm-10
- = f.text_area :new_project_guidelines, class: "form-control", rows: 10
- .hint
- Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
+ %hr
+ .row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0 Sign in/Sign up pages
- .form-actions
- = f.submit 'Save', class: 'btn btn-success append-right-10'
- - if @appearance.persisted?
- Preview last save:
- = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ .col-lg-8
+ .form-group
+ = f.label :title, class: 'col-form-label label-bold'
+ = f.text_field :title, class: "form-control"
+ .form-group
+ = f.label :description, class: 'col-form-label label-bold'
+ = f.text_area :description, class: "form-control", rows: 10
+ .hint
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
+ .form-group
+ = f.label :logo, class: 'col-form-label label-bold pt-0'
+ %p
+ - if @appearance.logo?
+ = image_tag @appearance.logo_path, class: 'appearance-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :logo_cache
+ = f.file_field :logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+
+ %hr
+ .row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0 New project pages
+
+ .col-lg-8
+ .form-group
+ = f.label :new_project_guidelines, class: 'col-form-label label-bold'
+ %p
+ = f.text_area :new_project_guidelines, class: "form-control", rows: 10
+ .hint
+ Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
+
+ .prepend-top-default.append-bottom-default
+ = f.submit 'Update appearance settings', class: 'btn btn-success'
+ - if @appearance.persisted?
+ Preview last save:
+ = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- - if @appearance.updated_at
- %span.float-right
- Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
+ - if @appearance.updated_at
+ %span.float-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/appearances/_system_header_footer_form.html.haml b/app/views/admin/appearances/_system_header_footer_form.html.haml
new file mode 100644
index 00000000000..4301ebd05af
--- /dev/null
+++ b/app/views/admin/appearances/_system_header_footer_form.html.haml
@@ -0,0 +1,33 @@
+- form = local_assigns.fetch(:form)
+
+%hr
+.row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('System header and footer')
+
+ .col-lg-8
+ .form-group
+ = form.label :header_message, _('Header message'), class: 'col-form-label label-bold'
+ = form.text_area :header_message, placeholder: _('State your message to activate'), class: "form-control js-autosize"
+ .form-group
+ = form.label :footer_message, _('Footer message'), class: 'col-form-label label-bold'
+ = form.text_area :footer_message, placeholder: _('State your message to activate'), class: "form-control js-autosize"
+ .form-group
+ .form-check
+ = form.check_box :email_header_and_footer_enabled, class: 'form-check-input'
+ = form.label :email_header_and_footer_enabled, class: 'label-bold' do
+ = _('Enable header and footer in emails')
+
+ .hint
+ = _('Add header and footer to emails. Please note that color settings will only be applied within the application interface')
+
+ .form-group.js-toggle-colors-container
+ %button.btn.btn-link.js-toggle-colors-link{ type: 'button' }
+ = _('Customize colors')
+ .form-group.js-toggle-colors-container.hide
+ = form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold'
+ = form.color_field :message_background_color, class: "form-control"
+ .form-group.js-toggle-colors-container.hide
+ = form.label :message_font_color, _('Font Color'), class: 'col-form-label label-bold'
+ = form.color_field :message_font_color, class: "form-control"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
index 454b779842c..ccf6f960cf2 100644
--- a/app/views/admin/appearances/show.html.haml
+++ b/app/views/admin/appearances/show.html.haml
@@ -1,9 +1,4 @@
- page_title "Appearance"
-
-%h3.page-title
- Appearance settings
-%p.light
- You can modify the look and feel of GitLab here
-%hr
+- @content_class = "limit-container-width" unless fluid_layout
= render 'form'
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 10bc3452d8b..65a24854583 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -15,7 +15,7 @@
= f.number_field :max_attachment_size, class: 'form-control'
.form-group
= f.label :receive_max_input_size, 'Maximum push size (MB)', class: 'label-light'
- = f.number_field :receive_max_input_size, class: 'form-control'
+ = f.number_field :receive_max_input_size, class: 'form-control qa-receive-max-input-size-field'
.form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light'
= f.number_field :session_expire_delay, class: 'form-control'
@@ -46,4 +46,4 @@
= f.label :user_show_add_ssh_key_message, class: 'form-check-label' do
Inform users without uploaded SSH keys that they can't push over SSH until one is added
- = f.submit 'Save changes', class: 'btn btn-success'
+ = f.submit 'Save changes', class: 'btn btn-success qa-save-changes-button'
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index fdaad1cf181..c99d7e9b8e9 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -13,7 +13,7 @@
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.form-group
- = f.label :auto_devops_domain, class: 'label-bold'
+ = f.label :auto_devops_domain, s_('AdminSettings|Auto DevOps domain'), class: 'label-bold'
= f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
.form-text.text-muted
= s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
new file mode 100644
index 00000000000..95d016a94a5
--- /dev/null
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -0,0 +1,11 @@
+= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :first_day_of_week, _('Default first day of the week'), class: 'label-bold'
+ = f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
+ .form-text.text-muted
+ = _('Default first day of the week in calendars and date pickers.')
+
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index c6c29ed1f21..7a2bbfcdc4d 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -11,7 +11,6 @@
.form-text.text-muted
Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
- %em (EXPERIMENTAL)
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'label-bold'
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 00000b86ab7..c468d69d7b8 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -56,3 +56,14 @@
= _('Configure Gitaly timeouts.')
.settings-content
= render 'gitaly'
+
+%section.settings.as-localization.no-animate#js-localization-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Localization')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Various localization settings.')
+ .settings-content
+ = render 'localization'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 65e4723afe6..fc9dd29b8ca 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -13,7 +13,7 @@
.settings-content
= render 'visibility_and_access'
-%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.qa-account-and-limit-settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Account and limit')
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 0a688b90f3a..395c469255e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -22,7 +22,7 @@
%span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
- .avatar-container.s40
+ .avatar-container.rect-avatar.s40
= group_icon(group, class: "avatar s40 d-none d-sm-block")
.title
= link_to [:admin, group], class: 'group-name' do
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index da2ebb08405..00d255846f9 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -15,7 +15,7 @@
= _('Group info:')
%ul.content-list
%li
- .avatar-container.s60
+ .avatar-container.rect-avatar.s60
= group_icon(@group, class: "avatar s60")
%li
%span.light= _('Name:')
@@ -104,7 +104,7 @@
- link_to_help = link_to(_("here"), help_page_path("user/permissions"))
= _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help }
- = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
+ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.prepend-top-10
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 50296a2afe7..5bc695aa7b5 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -19,7 +19,7 @@
.title
= link_to(admin_namespace_project_path(project.namespace, project)) do
.dash-project-avatar
- .avatar-container.s40
+ .avatar-container.rect-avatar.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
%span.project-full-name
%span.namespace-name
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index e4fc2985087..ecf2b1d60ba 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -18,19 +18,19 @@
.table-mobile-content
= link_to runner.short_sha, admin_runner_path(runner)
- .table-section.section-15
+ .table-section.section-20
.table-mobile-header{ role: 'rowheader' }= _('Description')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.description }
= runner.description
- .table-section.section-15
+ .table-section.section-10
.table-mobile-header{ role: 'rowheader' }= _('Version')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
= runner.version
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }= _('IP Address')
- .table-mobile-content
+ .table-mobile-content.str-truncated.has-tooltip{ title: runner.ip_address }
= runner.ip_address
.table-section.section-5
@@ -44,20 +44,21 @@
.table-section.section-5
.table-mobile-header{ role: 'rowheader' }= _('Jobs')
.table-mobile-content
- = runner.builds.count(:all)
+ = limited_counter_with_delimiter(runner.builds)
.table-section.section-10.section-wrap
.table-mobile-header{ role: 'rowheader' }= _('Tags')
.table-mobile-content
- - runner.tag_list.sort.each do |tag|
+ - runner.tags.map(&:name).sort.each do |tag|
%span.badge.badge-primary
= tag
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }= _('Last contact')
.table-mobile-content
- - if runner.contacted_at
- = time_ago_with_tooltip runner.contacted_at
+ - contacted_at = runner_contacted_at(runner)
+ - if contacted_at
+ = time_ago_with_tooltip contacted_at
- else
= _('Never')
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index e9e4e0847d3..2e23b748edb 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -92,6 +92,25 @@
= button_tag class: %w[btn btn-link] do
= runner_type.titleize
+ #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
+ %li.filter-dropdown-item{ data: { value: runner_type } }
+ = button_tag class: %w[btn btn-link] do
+ = runner_type.titleize
+
+ #js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
+ %button.btn.btn-link
+ = _('No Tag')
+ %li.divider.droplab-item-ignore
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value
+ %span.dropdown-light-content
+ {{name}}
+
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
@@ -106,8 +125,8 @@
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'rowheader' }= _('Type')
.table-section.section-10{ role: 'rowheader' }= _('Runner token')
- .table-section.section-15{ role: 'rowheader' }= _('Description')
- .table-section.section-15{ role: 'rowheader' }= _('Version')
+ .table-section.section-20{ role: 'rowheader' }= _('Description')
+ .table-section.section-10{ role: 'rowheader' }= _('Version')
.table-section.section-10{ role: 'rowheader' }= _('IP Address')
.table-section.section-5{ role: 'rowheader' }= _('Projects')
.table-section.section-5{ role: 'rowheader' }= _('Jobs')
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index b2163ee85fa..be7bfa958b2 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -1,36 +1,37 @@
-%li.flex-row
- .user-avatar
- = image_tag avatar_icon_for_user(user), class: "avatar", alt: ''
- .row-main-content
- .user-name.row-title.str-truncated-100
- = link_to user.name, [:admin, user]
- - if user.blocked?
- %span.badge.badge-danger blocked
- - if user.admin?
- %span.badge.badge-success Admin
- - if user.external?
- %span.badge.badge-secondary External
- - if user == current_user
- %span It's you!
- .row-second-line.str-truncated-100
- = mail_to user.email, user.email
- .controls
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- - unless user == current_user
- .dropdown.inline
- %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
+.gl-responsive-table-row{ role: 'row' }
+ .table-section.section-40
+ .table-mobile-header{ role: 'rowheader' }
+ = _('Name')
+ .table-mobile-content
+ = render 'user_detail', user: user
+ .table-section.section-25
+ .table-mobile-header{ role: 'rowheader' }
+ = _('Created on')
+ .table-mobile-content
+ = l(user.created_at.to_date, format: :admin)
+ .table-section.section-15
+ .table-mobile-header{ role: 'rowheader' }
+ = _('Last activity')
+ .table-mobile-content
+ = user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin)
+ .table-section.section-20.table-button-footer
+ .table-action-buttons
+ = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn btn-default'
+ - unless user == current_user
+ %button.dropdown-new.btn.btn-default{ type: 'button', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
- Settings
+ = _('Settings')
%li
- if user.ldap_blocked?
- %span.small Cannot unblock LDAP blocked users
+ %span.small
+ = s_('AdminUsers|Cannot unblock LDAP blocked users')
- elsif user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put
+ = link_to _('Unblock'), unblock_admin_user_path(user), method: :put
- else
- = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
+ = link_to _('Block'), block_admin_user_path(user), data: { confirm: "#{s_('AdminUsers|User will be blocked').upcase}! #{_('Are you sure')}?" }, method: :put
- if user.access_locked?
%li
= link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
@@ -42,7 +43,7 @@
target: '#delete-user-modal',
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
- username: user.name,
+ username: sanitize_name(user.name),
delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
@@ -51,6 +52,6 @@
target: '#delete-user-modal',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
- username: user.name,
+ username: sanitize_name(user.name),
delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
diff --git a/app/views/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml
new file mode 100644
index 00000000000..3319b4bad3a
--- /dev/null
+++ b/app/views/admin/users/_user_detail.html.haml
@@ -0,0 +1,17 @@
+.flex-list
+ .flex-row
+ = image_tag avatar_icon_for_user(user), class: 'avatar s32 d-none d-md-flex', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
+ .row-main-content
+ .row-title.str-truncated-100
+ = image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 prepend-top-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
+ = link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id }
+
+ = render_if_exists 'admin/users/user_detail_note', user: user
+
+ - user_badges_in_admin_section(user).each do |badge|
+ - css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present?
+ %span{ class: css_badge }
+ = badge[:text]
+
+ .row-second-line.str-truncated-100
+ = mail_to user.email, user.email, class: 'text-secondary'
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 600120c4f05..c3d5ce0fe70 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -2,72 +2,78 @@
- page_title "Users"
%div{ class: container_class }
- .prepend-top-default
+ .top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left
+ = icon('angle-left')
+ .fade-right
+ = icon('angle-right')
+ %ul.nav-links.nav.nav-tabs.scrolling-tabs
+ = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
+ = link_to admin_users_path do
+ = s_('AdminUsers|Active')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
+ = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
+ = link_to admin_users_path(filter: "admins") do
+ = s_('AdminUsers|Admins')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ = s_('AdminUsers|2FA Enabled')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
+ = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ = s_('AdminUsers|2FA Disabled')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
+ = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
+ = link_to admin_users_path(filter: 'external') do
+ = s_('AdminUsers|External')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
+ = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
+ = link_to admin_users_path(filter: "blocked") do
+ = s_('AdminUsers|Blocked')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
+ = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
+ = link_to admin_users_path(filter: "wop") do
+ = s_('AdminUsers|Without projects')
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
+ .nav-controls
+ = render_if_exists 'admin/users/admin_email_users'
+ = link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn btn-success btn-search float-right'
+
+ .filtered-search-block.row-content-block.border-top-0
= form_tag admin_users_path, method: :get do
- if params[:filter].present?
= hidden_field_tag "filter", h(params[:filter])
.search-holder
.search-field-holder
- = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+ = search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email or username'), class: 'form-control search-text-input js-search-input', spellcheck: false
- if @sort.present?
= hidden_field_tag :sort, @sort
= icon("search", class: "search-icon")
- = button_tag 'Search users' if Rails.env.test?
+ = button_tag s_('AdminUsers|Search users') if Rails.env.test?
.dropdown.user-sort-dropdown
- toggle_text = if @sort.present? then users_sort_options_hash[@sort] else sort_title_name end
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
- Sort by
+ = s_('AdminUsers|Sort by')
%li
- users_sort_options_hash.each do |value, title|
= link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do
= title
- = link_to 'New user', new_admin_user_path, class: 'btn btn-success btn-search'
- .top-area.scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.nav.nav-tabs.scrolling-tabs
- = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
- = link_to admin_users_path do
- Active
- %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
- = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
- = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
- = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
- = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
- = link_to admin_users_path(filter: 'external') do
- External
- %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
- = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
- = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
+ - if @users.empty?
+ .nothing-here-block.border-top-0
+ = s_('AdminUsers|No users found')
+ - else
+ .table-holder
+ .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
+ .table-section.section-40{ role: 'rowheader' }= _('Name')
+ .table-section.section-25{ role: 'rowheader' }= _('Created on')
+ .table-section.section-15{ role: 'rowheader' }= _('Last activity')
- %ul.flex-list.content-list
- - if @users.empty?
- %li
- .nothing-here-block No users found.
- - else
= render partial: 'admin/users/user', collection: @users
= paginate @users, theme: "gitlab"
#delete-user-modal
-
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index a758a63dfb3..8d9c083d223 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -3,7 +3,7 @@
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: [(award_state_class(awardable, awards, current_user))],
- data: { placement: "bottom", title: award_user_list(awards, current_user) } }
+ data: { title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
@@ -12,7 +12,7 @@
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': _('Add reaction'),
- data: { title: _('Add reaction'), placement: "bottom" } }
+ data: { title: _('Add reaction') } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 9de9143e8b1..c787d7420b7 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,7 +6,7 @@
- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
@@ -16,5 +16,5 @@
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml
new file mode 100644
index 00000000000..f38bdb2e5ed
--- /dev/null
+++ b/app/views/ci/status/_icon.html.haml
@@ -0,0 +1,16 @@
+- status = local_assigns.fetch(:status)
+- size = local_assigns.fetch(:size, 16)
+- type = local_assigns.fetch(:type, 'pipeline')
+- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
+- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
+- css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip"
+- title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label}
+- if type == 'commit'
+ - title = s_("PipelineStatusTooltip|Commit: %{ci_status}") % {ci_status: status.label}
+
+- if path
+ = link_to path, class: css_classes, title: title, data: { placement: tooltip_placement } do
+ = sprite_icon(status.icon, size: size)
+- else
+ %span{ class: css_classes, title: title, data: { placement: tooltip_placement } }
+ = sprite_icon(status.icon, size: size)
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index fa82611d9c1..90c59bec975 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1 +1,3 @@
= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want.')
+= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
+= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml
index 9238903aa10..c81d1d5b05a 100644
--- a/app/views/clusters/clusters/_buttons.html.haml
+++ b/app/views/clusters/clusters/_buttons.html.haml
@@ -1,6 +1,5 @@
--# This partial is overridden in EE
.nav-controls
- - if clusterable.can_create_cluster? && clusterable.clusters.empty?
+ - if clusterable.can_add_cluster?
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success js-add-cluster'
- else
%span.btn.btn-add-cluster.disabled.js-add-cluster
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
index c926ec258f0..cfdbfe2dea1 100644
--- a/app/views/clusters/clusters/_empty_state.html.haml
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -9,6 +9,6 @@
= clusterable.empty_state_help_text
= clusterable.learn_more_link
- - if clusterable.can_create_cluster?
+ - if clusterable.can_add_cluster?
.text-center
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success'
diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml
new file mode 100644
index 00000000000..9fb91a39387
--- /dev/null
+++ b/app/views/clusters/clusters/_form.html.haml
@@ -0,0 +1,46 @@
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
+ = form_errors(@cluster)
+ .form-group
+ %h5= s_('ClusterIntegration|Integration status')
+ %label.append-bottom-0.js-cluster-enable-toggle-area
+ %button{ type: 'button',
+ class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ "aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
+ disabled: !can?(current_user, :update_cluster, @cluster) }
+ = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
+ %span.toggle-icon
+ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
+ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
+
+ .form-group
+ %h5= s_('ClusterIntegration|Environment scope')
+ - if has_multiple_clusters?
+ = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
+ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
+ - else
+ = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true
+ - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain')
+ - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url }
+ .form-text.text-muted
+ %code *
+ = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe }
+
+ .form-group
+ %h5= s_('ClusterIntegration|Base domain')
+ = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus qa-base-domain'
+ .form-text.text-muted
+ - auto_devops_url = help_page_path('topics/autodevops/index')
+ - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
+ = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
+ - if @cluster.application_ingress_external_ip.present?
+ = s_('ClusterIntegration|Alternatively')
+ %code #{@cluster.application_ingress_external_ip}.nip.io
+ = s_('ClusterIntegration| can be used instead of a custom domain.')
+ - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip')
+ - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url }
+ = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe }
+
+ - if can?(current_user, :update_cluster, @cluster)
+ .form-group
+ = field.submit _('Save changes'), class: 'btn btn-success qa-save-domain'
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 73b11d509d3..a9299af8d78 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,6 +1,6 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
- %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
+ %button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
@@ -8,5 +8,5 @@
%h4= s_('ClusterIntegration|Did you know?')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-default{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
- Apply for credit
+ = s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml
deleted file mode 100644
index 5e451f60c9d..00000000000
--- a/app/views/clusters/clusters/_integration_form.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
- = form_errors(@cluster)
- .form-group
- %h5= s_('ClusterIntegration|Integration status')
- %label.append-bottom-0.js-cluster-enable-toggle-area
- %button{ type: 'button',
- class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
- "aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
- disabled: !can?(current_user, :update_cluster, @cluster) }
- = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
-
- - if has_multiple_clusters?
- .form-group
- %h5= s_('ClusterIntegration|Environment scope')
- = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
-
- - if can?(current_user, :update_cluster, @cluster)
- .form-group
- = field.submit _('Save changes'), class: 'btn btn-success'
-
- - unless has_multiple_clusters?
- %h5= s_('ClusterIntegration|Environment scope')
- %p
- %code *
- is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster.
- = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope')
diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
deleted file mode 100644
index e9f05eaf453..00000000000
--- a/app/views/clusters/clusters/gcp/_show.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-.form-group
- %label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Kubernetes cluster name')
- .input-group
- %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
- %span.input-group-append
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
-
-= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
- = form_errors(@cluster)
-
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
- .form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
- .input-group
- = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
- %span.input-group-append
- = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
-
- .form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
- .input-group
- = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
- %span.input-group-append.clipboard-addon
- = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
-
- .form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
- .input-group
- = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
- %span.input-group-append
- %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
- = s_('ClusterIntegration|Show')
- = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
-
- - if @cluster.allow_user_defined_namespace?
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
-
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
-
- .form-group
- = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 58d0a304363..9bab3bf56aa 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title 'Kubernetes'
-- page_title "Kubernetes Clusters"
+- breadcrumb_title _('Kubernetes')
+- page_title _('Kubernetes Clusters')
= render_gcp_signup_offer
@@ -9,12 +9,12 @@
- else
.top-area.adjust
.nav-text
- = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
+ = s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project')
= render 'clusters/clusters/buttons'
- if @has_ancestor_clusters
.bs-callout.bs-callout-info
- = s_("ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.")
+ = s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong
= link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index eeeef6bd824..6a8af23e5e8 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("Kubernetes Cluster")
+- breadcrumb_title _('Kubernetes')
+- page_title _('Kubernetes Cluster')
- active_tab = local_assigns.fetch(:active_tab, 'gcp')
= javascript_include_tag 'https://apis.google.com/js/api.js'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index b1aa8e5d477..1ef76ef801e 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path
+- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title @cluster.name
-- page_title _("Kubernetes Cluster")
+- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test?
@@ -16,6 +16,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
+ has_rbac: @cluster.platform_kubernetes_rbac? ? 'true': 'false',
cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
@@ -30,7 +31,7 @@
%section#cluster-integration
%h4= @cluster.name
= render 'banner'
- = render 'integration_form'
+ = render 'form'
.cluster-applications-table#js-cluster-applications
@@ -38,19 +39,16 @@
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- - if @cluster.managed?
- = render 'clusters/clusters/gcp/show'
- - else
- = render 'clusters/clusters/user/show'
+ = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster)
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
= render 'advanced_settings'
diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
deleted file mode 100644
index cac8e72edd3..00000000000
--- a/app/views/clusters/clusters/user/_show.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
- = form_errors(@cluster)
- .form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
-
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
- .form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold'
- = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
-
- .form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
-
- .form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
- .input-group
- = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
- %span.input-group-append.clipboard-addon
- .input-group-text
- %button.js-show-cluster-token.btn-blank{ type: 'button' }
- = s_('ClusterIntegration|Show')
-
- - if @cluster.allow_user_defined_namespace?
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
-
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
-
- .form-group
- = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml
new file mode 100644
index 00000000000..4a452b83112
--- /dev/null
+++ b/app/views/clusters/platforms/kubernetes/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for cluster, url: update_cluster_url_path, as: :cluster do |field|
+ = form_errors(cluster)
+
+ .form-group
+ - if cluster.managed?
+ %label.append-bottom-10{ for: 'cluster-name' }
+ = s_('ClusterIntegration|Kubernetes cluster name')
+ .input-group
+ %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true }
+ %span.input-group-append
+ = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
+ - else
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
+ .input-group
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
+
+ = field.fields_for :platform_kubernetes, platform do |platform_field|
+ .form-group
+ = platform_field.label :api_url, s_('ClusterIntegration|API URL')
+ .input-group
+ = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.managed?
+ - if cluster.managed?
+ %span.input-group-append
+ = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
+
+ .form-group
+ = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ .input-group
+ = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.managed?
+ - if cluster.managed?
+ %span.input-group-append.clipboard-addon
+ = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
+
+ .form-group
+ = platform_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.managed?
+ %span.input-group-append
+ %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+ - if cluster.managed?
+ = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
+
+ - if cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ .form-check
+ = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 9c246e19faa..4359a2c3c2b 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,7 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
+ = link_to dashboard_projects_path(rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss
.content_list
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 1050945b15a..ae67192cbcd 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -15,9 +15,11 @@
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
+ %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count)
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, data: {placement: 'right'} do
Starred projects
+ %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count)
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, data: {placement: 'right'} do
Explore projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 8d99f84755a..a05d0190efb 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,9 +1,9 @@
.page-title-holder
%h1.page-title= _('Snippets')
- - if current_user
+ - if current_user && current_user.snippets.any? || @snippets.any?
.page-title-controls
- = link_to "New snippet", new_snippet_path, class: "btn btn-success", title: "New snippet"
+ = link_to _("New snippet"), new_snippet_path, class: "btn btn-success", title: _("New snippet")
.top-area
%ul.nav-links.nav.nav-tabs
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 31d4b3da4f1..4dbda5c754b 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+
+= render_if_exists "shared/gold_trial_callout"
+
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 50f39f93283..19b06ba5cdd 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,6 +1,8 @@
- @hide_top_links = true
- page_title "Groups"
-- header_title "Groups", dashboard_groups_path
+- header_title "Groups", dashboard_groups_path
+
+= render_if_exists "shared/gold_trial_callout"
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index fdd5c19d562..afd46412fab 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Issues')
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 77cfa1271df..3e5f13b92e3 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,6 +2,8 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Merge Requests')
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index b876d6fd1f3..89212eb6bf9 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,5 +1,5 @@
= render 'shared/milestones/milestone',
- milestone_path: group_or_dashboard_milestone_path(milestone),
+ milestone_path: group_or_project_milestone_path(milestone),
issues_path: issues_dashboard_path(milestone_title: milestone.title),
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
milestone: milestone,
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index ae0e38bf0ee..13822d36f15 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -13,6 +13,8 @@
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
+ .nav-controls
+ = render 'shared/milestones/search_form'
.milestones
%ul.content-list
diff --git a/app/views/dashboard/projects/_starred_empty_state.html.haml b/app/views/dashboard/projects/_starred_empty_state.html.haml
new file mode 100644
index 00000000000..bea27f1a456
--- /dev/null
+++ b/app/views/dashboard/projects/_starred_empty_state.html.haml
@@ -0,0 +1,9 @@
+.row.empty-state
+ .col-12
+ .svg-content.svg-250
+ = image_tag 'illustrations/starred_empty.svg'
+ .text-content
+ %h4.text-center
+ = s_("StarredProjectsEmptyState|You don't have starred projects yet.")
+ %p.text-secondary
+ = s_("StarredProjectsEmptyState|Visit a project page and press on a star icon. Then, you can find the project on this page.")
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index deed774a4a5..446b4715b2d 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+= render_if_exists "shared/gold_trial_callout"
+
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 8933d9e31ff..3a45f6df017 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,8 +1,10 @@
- @hide_top_links = true
- @no_container = true
-- breadcrumb_title "Projects"
-- page_title "Starred Projects"
-- header_title "Projects", dashboard_projects_path
+- breadcrumb_title _("Projects")
+- page_title _("Starred Projects")
+- header_title _("Projects"), dashboard_projects_path
+
+= render_if_exists "shared/gold_trial_callout"
%div{ class: container_class }
= render "projects/last_push"
@@ -11,5 +13,4 @@
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
- %h3.page-title You don't have starred projects yet
- %p.slead Visit project page and press on star icon and it will appear on this page.
+ = render 'starred_empty_state'
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 6eb067da95c..b649fe91c24 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -3,6 +3,14 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
-= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
+- if current_user.snippets.exists?
+ = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-= render partial: 'snippets/snippets', locals: { link_project: true }
+.d-block.d-sm-none
+ &nbsp;
+ = link_to _("New snippet"), new_snippet_path, class: "btn btn-success btn-block", title: _("New snippet")
+
+- if current_user.snippets.exists?
+ = render partial: 'shared/snippets/list', locals: { link_project: true }
+- else
+ = render 'shared/empty_states/snippets', button_path: new_snippet_path
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d2593179f17..47729321961 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,6 +2,8 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Todos')
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
index 3d0a1f622a5..ccc3e734276 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -1,5 +1,5 @@
#content
- = email_default_heading("#{@resource.user.name}, you've added an additional email!")
+ = email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!")
%p Click the link below to confirm your email address (#{@resource.email})
#cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 4b8ad5acd5b..dd1edb5fdc9 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -7,7 +7,7 @@
= f.hidden_field :reset_password_token
.form-group
= f.label 'New password', for: "user_password"
- = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required'
+ = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required'
.form-group
= f.label 'Confirm new password', for: "user_password_confirmation"
= f.password_field :password_confirmation, class: "form-control bottom qa-password-confirmation", title: 'This field is required', required: true
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 796c0cadda8..f856773526d 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,3 +1,5 @@
+- server = local_assigns.fetch(:server)
+
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
= label_tag :username, "#{server['label']} Username"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 34d4293bd45..30ed7ed6b29 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,6 +1,6 @@
- page_title "Sign in"
-%div
+#signin-container
- if form_based_providers.any?
= render 'devise/shared/tabs_ldap'
- else
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index fefdf5f9531..f49cdfbf8da 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -7,7 +7,7 @@
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
- = f.label 'Two-Factor Authentication code', name: :otp_attempt
+ = f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 12271ee5adb..1b583ea85d6 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -5,7 +5,7 @@
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- has_icon = provider_has_icon?(provider)
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login qa-saml-login-button', id: "oauth-login-#{provider}" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn d-flex align-items-center omniauth-btn text-left oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
- if has_icon
= provider_image_tag(provider)
%span
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 004a3528d4b..9c7ca6ebbd4 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -3,12 +3,12 @@
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= devise_error_messages!
- .form-group
+ .name.form-group
= f.label :name, 'Full name', class: 'label-bold'
- = f.text_field :name, class: "form-control top qa-new-user-name", required: true, title: "This field is required."
+ = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji", required: true, title: _("This field is required.")
.username.form-group
= f.label :username, class: 'label-bold'
- = f.text_field :username, class: "form-control middle qa-new-user-username", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_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/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 74791b81ccd..dae9a7acf6b 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -31,7 +31,7 @@
%strong= t scope, scope: [:doorkeeper, :scopes]
.text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
- = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
+ = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state
diff --git a/app/views/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml
index c4ae7befe4e..666dab61f40 100644
--- a/app/views/email_rejection_mailer/rejection.html.haml
+++ b/app/views/email_rejection_mailer/rejection.html.haml
@@ -1,5 +1,5 @@
%p
- Unfortunately, your email message to GitLab could not be processed.
+ = _("Unfortunately, your email message to GitLab could not be processed.")
= markdown @reason
= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index 0e13b2a6473..8d940ef1293 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,4 +1,4 @@
-Unfortunately, your email message to GitLab could not be processed.
+= _("Unfortunately, your email message to GitLab could not be processed.")
\
= @reason
= render_if_exists 'shared/additional_email_text'
diff --git a/app/views/events/_events.html.haml b/app/views/events/_events.html.haml
index 68c19df092d..e1b7804c5a7 100644
--- a/app/views/events/_events.html.haml
+++ b/app/views/events/_events.html.haml
@@ -1 +1,18 @@
-= render partial: 'events/event', collection: @events
+- illustration_path = 'illustrations/profile-page/activity.svg'
+- current_user_empty_message_header = s_('UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!')
+- primary_button_label = _('New group')
+- primary_button_link = new_group_path
+- secondary_button_label = _('Explore groups')
+- secondary_button_link = explore_groups_path
+- visitor_empty_message = _('No activities found')
+
+- if @events.present?
+ = render partial: 'events/event', collection: @events
+- else
+ = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: illustration_path,
+ current_user_empty_message_header: current_user_empty_message_header,
+ primary_button_label: primary_button_label,
+ primary_button_link: primary_button_link,
+ secondary_button_label: secondary_button_label,
+ secondary_button_link: secondary_button_link,
+ visitor_empty_message: visitor_empty_message }
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index fb0d2c3b8b0..90ed8e41d32 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -21,6 +21,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/index.html.haml b/app/views/explore/groups/index.html.haml
index a3eafc61d0a..869be4e8581 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -2,6 +2,8 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/groups_head'
- else
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 94fc4ac21d2..d23c8301b10 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -7,4 +7,4 @@
- else
= render 'explore/head'
-= render partial: 'snippets/snippets', locals: { link_project: true }
+= render partial: 'shared/snippets/list', locals: { link_project: true }
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 82a497289f3..13df1e57125 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,7 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
+ = link_to group_path(@group, rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
.content_list
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 6219da2c715..39c0c113793 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,17 +1,58 @@
-.group-home-panel.text-center.border-bottom
- %div{ class: container_class }
- .avatar-container.s70.group-avatar
- = group_icon(@group, class: "avatar s70 avatar-tile")
- %h1.group-title
- = @group.name
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+- can_create_subgroups = can?(current_user, :create_subgroup, @group)
- - if @group.description.present?
- .group-home-desc
- = markdown_field(@group, :description)
+.group-home-panel
+ .row.mb-3
+ .home-panel-title-row.col-md-12.col-lg-6.d-flex
+ .avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
+ = group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64)
+ .d-flex.flex-column.flex-wrap.align-items-baseline
+ .d-inline-flex.align-items-baseline
+ %h1.home-panel-title.prepend-top-8.append-bottom-5
+ = @group.name
+ %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false, options: {class: 'icon'})
+ .home-panel-metadata.d-flex.align-items-center.text-secondary
+ %span
+ = _("Group")
+ - if current_user
+ %span.access-request-links.prepend-left-8
+ = render 'shared/members/access_request_links', source: @group
- - if current_user
- .group-buttons
- = render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
+ - if current_user
+ .group-buttons
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn'
+ - if can? current_user, :create_projects, @group
+ - new_project_label = _("New project")
+ - new_subgroup_label = _("New subgroup")
+ - if can_create_subgroups
+ .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.prepend-top-default.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
+ %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
+ = sprite_icon("arrow-down", css_class: "icon dropdown-btn-icon")
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
+ %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_project_label
+ %span= s_("GroupsTree|Create a project in this group.")
+ %li.divider.droplap-item-ignore
+ %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_subgroup_label
+ %span= s_("GroupsTree|Create a subgroup in this group.")
+ - else
+ = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
+
+ - if @group.description.present?
+ .group-home-desc.mt-1
+ .home-panel-description
+ .home-panel-description-markdown.read-more-container
+ = markdown_field(@group, :description)
+ %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
+ = _("Read more")
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 13d584f5f1d..2af3e861587 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -23,7 +23,7 @@
Members with access to
%strong= @group.name
%span.badge= @members.total_count
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
.position-relative.append-right-8
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 4df3d831942..5cf3193bc62 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -6,15 +6,10 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
-- if @labels.present? && can_admin_label
- - content_for(:header_content) do
- .nav-controls
- = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success"
-
- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- = render 'shared/labels/nav'
+ = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
.labels-container.prepend-top-5
- if @labels.any?
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index a9ce2fe5ab0..808bb1309b1 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -7,7 +7,7 @@
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 39e3af5f6d2..b0ba846f204 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,20 +1,20 @@
-= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+ = form_errors(@milestone)
.row
- = form_errors(@milestone)
-
.col-md-6
.form-group.row
- = f.label :title, "Title", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :title, "Title"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, "Description", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :description, "Description"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
- .clearfix
- .error-alert
-
+ .clearfix
+ .error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
@@ -24,4 +24,3 @@
- else
= f.submit 'Update milestone', class: "btn-success btn"
= link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel"
-
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index af4fe8f2ef8..b6fb908c8f6 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
+ = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success"
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
index bcfb6d99716..806a24e2bbc 100644
--- a/app/views/groups/runners/_group_runners.html.haml
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -21,6 +21,6 @@
- else
%h4.underlined-title
- = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count }
+ = _('Available group Runners: %{runners}').html_safe % { runners: @group.runners.count }
%ul.bordered-list
= render partial: 'groups/runners/runner', collection: @group.runners, as: :runner
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index 0424ece037d..9ed71d19d32 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -20,7 +20,7 @@
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.form-group.prepend-top-default.append-bottom-20
- .avatar-container.s90
+ .avatar-container.rect-avatar.s90
= group_icon(@group, alt: '', class: 'avatar group-avatar s90')
= f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
= render 'shared/choose_group_avatar_button', f: f
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index cc294f6a931..77fe88dacb7 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,66 +1,41 @@
- @no_container = true
- breadcrumb_title _("Details")
-- can_create_subgroups = can?(current_user, :create_subgroup, @group)
+- @content_class = "limit-container-width" unless fluid_layout
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
-= render 'groups/home_panel'
-
-.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
- .top-area.group-nav-container
- .group-search
- = render "shared/groups/search_form"
- - if can? current_user, :create_projects, @group
- - new_project_label = _("New project")
- - new_subgroup_label = _("New subgroup")
- - if can_create_subgroups
- .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
- %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
- = icon("caret-down", class: "dropdown-btn-icon")
- %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
- %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
- .menu-item
- .icon-container
- = icon("check", class: "list-item-checkmark")
- .description
- %strong= new_project_label
- %span= s_("GroupsTree|Create a project in this group.")
- %li.divider.droplap-item-ignore
- %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
- .menu-item
- .icon-container
- = icon("check", class: "list-item-checkmark")
- .description
- %strong= new_subgroup_label
- %span= s_("GroupsTree|Create a subgroup in this group.")
- - else
- = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
-
- .scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
- %li.js-subgroups_and_projects-tab
- = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
- = _("Subgroups and projects")
- %li.js-shared-tab
- = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
- = _("Shared projects")
- %li.js-archived-tab
- = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
- = _("Archived projects")
-
- .nav-controls
- = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
-
- .tab-content
- #subgroups_and_projects.tab-pane
- = render "subgroups_and_projects", group: @group
-
- #shared.tab-pane
- = render "shared_projects", group: @group
-
- #archived.tab-pane
- = render "archived_projects", group: @group
+%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ = render 'groups/home_panel'
+
+ .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
+ .top-area.group-nav-container
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
+ %li.js-subgroups_and_projects-tab
+ = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
+ = _("Subgroups and projects")
+ %li.js-shared-tab
+ = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
+ = _("Shared projects")
+ %li.js-archived-tab
+ = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
+ = _("Archived projects")
+
+ .nav-controls.d-block.d-md-flex
+ .group-search
+ = render "shared/groups/search_form"
+
+ = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
+
+ .tab-content
+ #subgroups_and_projects.tab-pane
+ = render "subgroups_and_projects", group: @group
+
+ #shared.tab-pane
+ = render "shared_projects", group: @group
+
+ #archived.tab-pane
+ = render "archived_projects", group: @group
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 28ffb2dd63c..efb3815b257 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -356,6 +356,18 @@
%td.shortcut
%kbd l
%td Change Label
+ %tr
+ %td.shortcut
+ %kbd ]
+ \/
+ %kbd j
+ %td Move to next file
+ %tr
+ %td.shortcut
+ %kbd [
+ \/
+ %kbd k
+ %td Move to previous file
%tbody.hidden-shortcut{ style: 'display:none' }
%tr
%th
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index b24d6e27536..057225d021f 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -4,7 +4,7 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
-#ide.ide-loading{ data: ide_data() }
+#ide.ide-loading{ data: ide_data }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index f4a29ed18dc..b05c039c85c 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -1,56 +1,9 @@
- provider = local_assigns.fetch(:provider)
- provider_title = Gitlab::ImportSources.title(provider)
-%p.light
- = import_githubish_choose_repository_message
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- = import_all_githubish_repositories_button_label
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th= _('From %{provider_title}') % { provider_title: provider_title }
- %th= _('To GitLab')
- %th= _('Status')
- %tbody
- - @already_added_projects.each do |project|
- %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
- %td
- = provider_project_link(provider, project.import_source)
- %td
- = link_to project.full_path, [project.namespace.becomes(Namespace), project]
- %td.job-status
- = render 'import/project_status', project: project
-
- - @repos.each do |repo|
- %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
- %td
- = provider_project_link(provider, repo.full_name)
- %td.import-target
- %fieldset.row
- .input-group
- .project-path.input-group-prepend
- - if current_user.can_select_namespace?
- - selected = params[:namespace_id] || :current_user
- - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace qa-project-namespace-select', tabindex: 1 }
- - else
- = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
- %span.input-group-prepend
- .input-group-text /
- = text_field_tag :path, sanitize_project_name(repo.name), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- = has_ci_cd_only_params? ? _('Connect') : _('Import')
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
- import_path: url_for([:import, provider]),
- ci_cd_only: has_ci_cd_only_params?.to_s } }
+#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
+ can_select_namespace: current_user.can_select_namespace?.to_s,
+ ci_cd_only: has_ci_cd_only_params?.to_s,
+ repos_path: url_for([:status, :import, provider, format: :json]),
+ jobs_path: url_for([:realtime_changes, :import, provider, format: :json]),
+ import_path: url_for([:import, provider, format: :json]) } }
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index ef69197e453..9280f12e187 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -56,7 +56,7 @@
.project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :extra_group
- - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.project_key, path: repo.project_key) } : {}
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {}
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 6ff25f2c842..cf32c5c9387 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -4,7 +4,7 @@
- header_title _("Projects"), root_path
%h3.page-title
- = icon 'github', text: import_github_title
+ = icon 'github', text: _('Import repositories from GitHub')
- if github_import_configured?
%p
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index be057be6d1a..ee295e70cce 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -2,7 +2,7 @@
- page_title title
- breadcrumb_title title
- header_title _("Projects"), root_path
-%h3.page-title
- = icon 'github', text: import_github_title
+%h3.page-title.mb-0
+ = icon 'github', class: 'fa-2x', text: _('Import repositories from GitHub')
= render 'import/githubish_status', provider: 'github'
diff --git a/app/views/import/manifest/status.html.haml b/app/views/import/manifest/status.html.haml
index 5b2e1005398..3d4abc32b88 100644
--- a/app/views/import/manifest/status.html.haml
+++ b/app/views/import/manifest/status.html.haml
@@ -7,7 +7,7 @@
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
- = import_all_githubish_repositories_button_label
+ = _('Import all repositories')
= icon("spinner spin", class: "loading-icon")
.table-responsive
diff --git a/app/views/instance_statistics/conversational_development_index/_callout.html.haml b/app/views/instance_statistics/conversational_development_index/_callout.html.haml
index 33a4dab1e00..a4256e23979 100644
--- a/app/views/instance_statistics/conversational_development_index/_callout.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_callout.html.haml
@@ -2,12 +2,12 @@
.user-callout{ data: { uid: 'convdev_intro_callout_dismissed' } }
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
- 'aria-label' => 'Dismiss ConvDev introduction' }
+ 'aria-label' => _('Dismiss ConvDev introduction') }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.user-callout-copy
%h4
- Introducing Your Conversational Development Index
+ = _('Introducing Your Conversational Development Index')
%p
- Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.
+ = _('Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
.svg-container.convdev
= custom_icon('convdev_overview')
diff --git a/app/views/instance_statistics/conversational_development_index/_card.html.haml b/app/views/instance_statistics/conversational_development_index/_card.html.haml
index 57eda06630b..76af55dcf7a 100644
--- a/app/views/instance_statistics/conversational_development_index/_card.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_card.html.haml
@@ -9,11 +9,11 @@
.board-card-score
.board-card-score-value
= format_score(card.instance_score)
- .board-card-score-name You
+ .board-card-score-name= _('You')
.board-card-score
.board-card-score-value
= format_score(card.leader_score)
- .board-card-score-name Lead
+ .board-card-score-name= _('Lead')
.board-card-score-big
= number_to_percentage(card.percentage_score, precision: 1)
.board-card-buttons
diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
index dd795aee135..4e8f34cd574 100644
--- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
@@ -1,7 +1,7 @@
.container.convdev-empty
.col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_data')
- %h4 Data is still calculating...
+ %h4= _('Data is still calculating...')
%p
- In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.
- = link_to 'Learn more', help_page_path('user/instance_statistics/convdev'), target: '_blank'
+ = _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.')
+ = link_to _('Learn more'), help_page_path('user/instance_statistics/convdev'), target: '_blank'
diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml
index 1e7db4982d6..23f90b876a0 100644
--- a/app/views/instance_statistics/conversational_development_index/index.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/index.html.haml
@@ -17,9 +17,9 @@
%h2.convdev-header-title{ class: "convdev-#{score_level(@metric.average_percentage_score)}-score" }
= number_to_percentage(@metric.average_percentage_score, precision: 1)
.convdev-header-subtitle
- index
+ = _('index')
%br
- score
+ = _('score')
= link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev')
.convdev-cards.board-card-container
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 08a6359f777..11e83ddfe64 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -11,20 +11,20 @@
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
-# Open Graph - http://ogp.me/
- %meta{ property: 'og:type', content: "object" }
- %meta{ property: 'og:site_name', content: site_name }
- %meta{ property: 'og:title', content: page_title }
+ %meta{ property: 'og:type', content: "object" }
+ %meta{ property: 'og:site_name', content: site_name }
+ %meta{ property: 'og:title', content: page_title }
%meta{ property: 'og:description', content: page_description }
- %meta{ property: 'og:image', content: page_image }
+ %meta{ property: 'og:image', content: page_image }
%meta{ property: 'og:image:width', content: '64' }
%meta{ property: 'og:image:height', content: '64' }
- %meta{ property: 'og:url', content: request.base_url + request.fullpath }
+ %meta{ property: 'og:url', content: request.base_url + request.fullpath }
-# Twitter Card - https://dev.twitter.com/cards/types/summary
- %meta{ property: 'twitter:card', content: "summary" }
- %meta{ property: 'twitter:title', content: page_title }
- %meta{ property: 'twitter:description', content: page_description }
- %meta{ property: 'twitter:image', content: page_image }
+ %meta{ property: 'twitter:card', content: "summary" }
+ %meta{ property: 'twitter:title', content: page_title }
+ %meta{ property: 'twitter:description', content: page_description }
+ %meta{ property: 'twitter:image', content: page_image }
= page_card_meta_tags
%title= page_title(site_name)
@@ -33,11 +33,13 @@
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= stylesheet_link_tag "application", media: "all"
- = stylesheet_link_tag "print", media: "print"
- = stylesheet_link_tag "test", media: "all" if Rails.env.test?
+ = stylesheet_link_tag "print", media: "print"
+ = stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
= stylesheet_link_tag 'csslab' if Feature.enabled?(:csslab)
+ = stylesheet_link_tag "highlight/themes/#{user_color_scheme}", media: "all"
+
= Gon::Base.render_data
- if content_for?(:library_javascripts)
@@ -61,10 +63,10 @@
%meta{ name: 'theme-color', content: '#474D57' }
-# Apple Safari/iOS home screen icons
- = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon'
- = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
+ = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon'
+ = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
- = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
+ = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
%link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' }
-# Windows 8 pinned site tile
diff --git a/app/views/layouts/_init_client_detection_flags.html.haml b/app/views/layouts/_init_client_detection_flags.html.haml
new file mode 100644
index 00000000000..c729f8aa696
--- /dev/null
+++ b/app/views/layouts/_init_client_detection_flags.html.haml
@@ -0,0 +1,7 @@
+- client = client_js_flags
+
+- if client
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ gl = window.gl || {};
+ gl.client = #{client.to_json};
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index ddc1cdb24b5..e13490ed410 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -49,9 +49,10 @@
%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;" }  
+ %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;" }
+ = html_header_message
= header_logo
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
@@ -72,3 +73,6 @@
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
= yield :additional_footer
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ = html_footer_message
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 1f4d24d996c..043cca6ad38 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,10 +1,13 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
+ %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
+ = render "layouts/init_client_detection_flags"
= render 'peek/bar'
- = render partial: "layouts/header/default", locals: { project: @project, group: @group }
+ = header_message
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'layouts/page', sidebar: sidebar, nav: nav
+ = footer_message
= yield :scripts_body
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 6003d973c88..2f3c13aaf6e 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -2,6 +2,7 @@
%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
%body.ui-indigo.login-page.application.navless.qa-login-page{ data: { page: body_data_page } }
+ = header_message
.page-wrap
= render "layouts/header/empty"
.login-page-broadcast
@@ -34,3 +35,4 @@
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://about.gitlab.com/"
+ = footer_message
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 663e5b24368..6c9c8aa4431 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -2,6 +2,7 @@
%html{ lang: "en", class: system_message_class }
= render "layouts/head"
%body.ui-indigo.login-page.application.navless
+ = header_message
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
@@ -15,3 +16,4 @@
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://about.gitlab.com/"
+ = footer_message
diff --git a/app/views/layouts/empty_mailer.html.haml b/app/views/layouts/empty_mailer.html.haml
new file mode 100644
index 00000000000..a25dcefd445
--- /dev/null
+++ b/app/views/layouts/empty_mailer.html.haml
@@ -0,0 +1,5 @@
+= html_header_message
+
+= yield
+
+= html_footer_message
diff --git a/app/views/layouts/empty_mailer.text.erb b/app/views/layouts/empty_mailer.text.erb
new file mode 100644
index 00000000000..6ab0dbead07
--- /dev/null
+++ b/app/views/layouts/empty_mailer.text.erb
@@ -0,0 +1,5 @@
+<%= text_header_message %>
+
+<%= yield -%>
+
+<%= text_footer_message %>
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index bfbfeee7c4b..1d40b78fa83 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,7 +1,7 @@
- page_title @group.name
- page_description @group.description unless page_description
- header_title group_title(@group) unless header_title
-- nav "group"
+- nav "group"
- @left_sidebar = true
- content_for :page_specific_javascripts do
diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml
index 2dfc787b7a8..348ce18b122 100644
--- a/app/views/layouts/header/_empty.html.haml
+++ b/app/views/layouts/header/_empty.html.haml
@@ -1,4 +1,2 @@
%header.navbar.fixed-top.navbar-empty
- .container
- .mx-auto
- = brand_header_logo
+ = brand_header_logo
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 953c0e7f46c..cd9128c452b 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -2,5 +2,8 @@
- if current_user_menu?(:help)
%li
= link_to _("Help"), help_path
+ %li.divider
+ %li
+ = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index e42251f9ec8..5a66b02c048 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,4 +1,4 @@
-%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } }
+%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index 8e11174f8d7..f8032f3262b 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -1,4 +1,8 @@
+<%= text_header_message %>
+
<%= yield -%>
-- <%# signature marker %>
<%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %>
+
+<%= text_footer_message %>
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index ddd30efe062..f659c89dd30 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -10,7 +10,7 @@
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } }
= _('Groups')
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 5f15ba87729..2fdd65f639b 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -207,7 +207,7 @@
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: 'application_settings#show') do
- = link_to admin_application_settings_path, title: _('General') do
+ = link_to admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
%span
= _('General')
= nav_link(path: 'application_settings#integrations') do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 477030a20c1..21ea9f3b2f3 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -6,7 +6,7 @@
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
- .avatar-container.s40.group-avatar
+ .avatar-container.rect-avatar.s40.group-avatar
= group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
@@ -36,7 +36,7 @@
%span
= _('Activity')
- = render_if_exists 'groups/sidebar/security_dashboard'
+ = render_if_exists 'groups/sidebar/security_dashboard' # EE-specific
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'analytics#show') do
@@ -103,19 +103,6 @@
= _('Merge Requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
- - if group_sidebar_link?(:group_members)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group) do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name.qa-group-members-item
- = _('Members')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to group_group_members_path(@group) do
- %strong.fly-out-top-item-name
- = _('Members')
-
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
@@ -129,6 +116,19 @@
%strong.fly-out-top-item-name
= _('Kubernetes')
+ - if group_sidebar_link?(:group_members)
+ = nav_link(path: 'group_members#index') do
+ = link_to group_group_members_path(@group) do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name.qa-group-members-item
+ = _('Members')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_group_members_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Members')
+
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 69167edb1df..1e3bb8f1224 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -3,7 +3,7 @@
.context-header
= link_to profile_path, title: _('Profile Settings') do
.avatar-container.s40.settings-avatar
- = sprite_icon('user', size: 24)
+ = image_tag avatar_icon_for_user(current_user, 40), class: "avatar s40 avatar-tile", alt: current_user.name
.sidebar-context-title User Settings
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 59557c70904..7b492efeb09 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -3,7 +3,7 @@
- can_edit = can?(current_user, :admin_project, @project)
.context-header
= link_to project_path(@project), title: @project.name do
- .avatar-container.s40.project-avatar
+ .avatar-container.rect-avatar.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
.sidebar-context-title
= @project.name
@@ -26,10 +26,10 @@
%span= _('Details')
= nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
+ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity qa-activity-link' do
%span= _('Activity')
- - if project_nav_tab?(:releases) && Feature.enabled?(:releases_page)
+ - if project_nav_tab?(:releases)
= nav_link(controller: :releases) do
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
%span= _('Releases')
@@ -43,7 +43,7 @@
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths) do
- = link_to project_tree_path(@project), class: 'shortcuts-tree' do
+ = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do
.nav-icon-container
= sprite_icon('doc-text')
%span.nav-item-name
@@ -64,7 +64,7 @@
= _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
- = link_to project_branches_path(@project) do
+ = link_to project_branches_path(@project), class: 'qa-branches-link' do
= _('Branches')
= nav_link(controller: [:tags]) do
@@ -146,7 +146,7 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests qa-merge-requests-link' do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
@@ -170,7 +170,7 @@
= _('CI / CD')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" }) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
= _('CI / CD')
@@ -201,7 +201,7 @@
- if project_nav_tab? :operations
= nav_link(controller: sidebar_operations_paths) do
- = link_to sidebar_operations_link_path, class: 'shortcuts-operations' do
+ = link_to sidebar_operations_link_path, class: 'shortcuts-operations qa-link-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
@@ -227,6 +227,12 @@
%span
= _('Environments')
+ - if project_nav_tab?(:error_tracking)
+ = nav_link(controller: :error_tracking) do
+ = link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do
+ %span
+ = _('Error Tracking')
+
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
@@ -275,19 +281,34 @@
%strong.fly-out-top-item-name
= _('Registry')
- - if project_nav_tab? :wiki
+ - if project_nav_tab?(:wiki)
+ - wiki_url = project_wiki_path(@project, :home)
= nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
+ = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
= _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
- = link_to get_project_wiki_path(@project) do
+ = link_to wiki_url do
%strong.fly-out-top-item-name
= _('Wiki')
+ - if project_nav_tab?(:external_wiki)
+ - external_wiki_url = @project.external_wiki.external_wiki_url
+ = nav_link do
+ = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
+ .nav-icon-container
+ = sprite_icon('issue-external')
+ %span.nav-item-name
+ = _('External Wiki')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: "fly-out-top-item" } ) do
+ = link_to external_wiki_url do
+ %strong.fly-out-top-item-name
+ = _('External Wiki')
+
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
@@ -339,7 +360,10 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
- = render_if_exists 'projects/sidebar/settings_operations'
+ - if settings_operations_available?
+ = nav_link(controller: [:operations]) do
+ = link_to project_settings_operations_path(@project), title: _('Operations') do
+ = _('Operations')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 1c3e05e07f4..8dff12c1b7f 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -7,6 +7,7 @@
= yield :head
%body
.content
+ = html_header_message
= yield
.footer{ style: "margin-top: 10px;" }
%p
@@ -30,3 +31,4 @@
adjust your notification settings.
= email_action @target_url
+ = html_footer_message
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
index 9dc490efa9a..248916fba63 100644
--- a/app/views/layouts/notify.text.erb
+++ b/app/views/layouts/notify.text.erb
@@ -1,3 +1,5 @@
+<%= text_header_message %>
+
<%= yield -%>
-- <%# signature marker %>
@@ -10,3 +12,5 @@
<% end -%>
<%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
+
+<%= text_footer_message -%>
diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml
index 6418500d5d1..5f986c81ff4 100644
--- a/app/views/layouts/snippets.html.haml
+++ b/app/views/layouts/snippets.html.haml
@@ -1,4 +1,4 @@
-- header_title _("Snippets"), snippets_path
+- header_title _("Snippets"), snippets_path
- content_for :page_specific_javascripts do
- if @snippet && current_user
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index 50209c46ed1..5a67214059c 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -3,7 +3,7 @@
<% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%>
-<%= note.author_name -%>
+<%= sanitize_name(note.author_name) -%>
<% if discussion.new_discussion? -%>
<%= " started a new discussion" -%>
<% else -%>
@@ -16,7 +16,7 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
-<%= "#{note.author_name} commented:" -%>
+<%= "#{sanitize_name(note.author_name)} commented:" -%>
<% end -%>
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
index 695780c3145..bf863952478 100644
--- a/app/views/notify/autodevops_disabled_email.text.erb
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%>
- Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index b7284dd819b..eb148d72da1 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{@updated_by.name}
+ Issue was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index b35d4b7502d..b1f0a3f37ec 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{@updated_by.name}
+Issue was closed by #{sanitize_name(@updated_by.name)}
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 44e018304e1..2aa753e0d55 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index c4e06cb3cb1..1094d584a1c 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/import_issues_csv_email.html.haml b/app/views/notify/import_issues_csv_email.html.haml
new file mode 100644
index 00000000000..f30d2b5f078
--- /dev/null
+++ b/app/views/notify/import_issues_csv_email.html.haml
@@ -0,0 +1,18 @@
+- text_style = 'font-size:16px; text-align:center; line-height:30px;'
+
+%p{ style: text_style }
+ Your CSV import for project
+ %a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none;" }
+ = @project.full_name
+ has been completed.
+
+%p{ style: text_style }
+ #{pluralize(@results[:success], 'issue')} imported.
+
+- if @results[:error_lines].present?
+ %p{ style: text_style }
+ Errors found on line #{'number'.pluralize(@results[:error_lines].size)}: #{@results[:error_lines].join(', ')}. Please check if these lines have an issue title.
+
+- if @results[:parse_error]
+ %p{ style: text_style }
+ Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.
diff --git a/app/views/notify/import_issues_csv_email.text.erb b/app/views/notify/import_issues_csv_email.text.erb
new file mode 100644
index 00000000000..1117f90714d
--- /dev/null
+++ b/app/views/notify/import_issues_csv_email.text.erb
@@ -0,0 +1,11 @@
+Your CSV import for project <%= @project.full_name %> (<%= project_url(@project) %>) has been completed.
+
+<%= pluralize(@results[:success], 'issue') %> imported.
+
+<% if @results[:error_lines].present? %>
+Errors found on line <%= 'number'.pluralize(@results[:error_lines].size) %>: <%= @results[:error_lines].join(', ') %>. Please check if these lines have an issue title.
+<% end %>
+
+<% if @results[:parse_error] %>
+Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.
+<% end %>
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
index 472c31e9a5e..b766cb1a523 100644
--- a/app/views/notify/issue_moved_email.html.haml
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -1,6 +1,9 @@
%p
Issue was moved to another project.
-%p
- New issue:
- = link_to project_issue_url(@new_project, @new_issue) do
- = @new_issue.title
+- if @can_access_project
+ %p
+ New issue:
+ = link_to project_issue_url(@new_project, @new_issue) do
+ = @new_issue.title
+- else
+ You don't have access to the project.
diff --git a/app/views/notify/issue_moved_email.text.erb b/app/views/notify/issue_moved_email.text.erb
index 66ede43635b..985e689aa9d 100644
--- a/app/views/notify/issue_moved_email.text.erb
+++ b/app/views/notify/issue_moved_email.text.erb
@@ -1,4 +1,8 @@
Issue was moved to another project.
+<% if @can_access_project %>
New issue location:
<%= project_issue_url(@new_project, @new_issue) %>
+<% else %>
+You don't have access to the project.
+<% end %>
diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml
index b6051b11cea..66e73a9b03f 100644
--- a/app/views/notify/issue_status_changed_email.html.haml
+++ b/app/views/notify/issue_status_changed_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was #{@issue_status} by #{@updated_by.name}
+ Issue was #{@issue_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb
index 4200881f7e8..f38b09e9820 100644
--- a/app/views/notify/issue_status_changed_email.text.erb
+++ b/app/views/notify/issue_status_changed_email.text.erb
@@ -1,4 +1,4 @@
-Issue was <%= @issue_status %> by <%= @updated_by.name %>
+Issue was <%= @issue_status %> by <%= sanitize_name(@updated_by.name) %>
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
index 9c5ee0eaf26..ddb4a7b3d2c 100644
--- a/app/views/notify/member_access_requested_email.text.erb
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= sanitize_name(member.user.name) %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
index cef87101427..c824533eac2 100644
--- a/app/views/notify/member_invite_accepted_email.text.erb
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= member.invite_email %>, now known as <%= sanitize_name(member.user.name) %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
index 0a6393355be..d944c3b4a50 100644
--- a/app/views/notify/member_invited_email.text.erb
+++ b/app/views/notify/member_invited_email.text.erb
@@ -1,4 +1,4 @@
-You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index b487e26b122..ffb416abf72 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index ae2a2933865..b9b9e0c3ad7 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index dcdd6db69d6..0c7bf1bb044 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 661c23bcbe2..045a43cbc84 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml
index 4b9350c4e88..b857705e01f 100644
--- a/app/views/notify/new_gpg_key_email.html.haml
+++ b/app/views/notify/new_gpg_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new GPG key was added to your account:
%p
diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb
index 80b5a1fd7ff..92ea851eee4 100644
--- a/app/views/notify/new_gpg_key_email.text.erb
+++ b/app/views/notify/new_gpg_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new GPG key was added to your account:
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index 3c716f77296..58a2bcbe5eb 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -1,7 +1,7 @@
New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
+Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 23213106c5b..173091e4a80 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -1,7 +1,7 @@
You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
-Assignee: <%= @issue.assignee_list %>
+Author: <%= sanitize_name(@issue.author_name) %>
+Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 6fcebb22fc4..96a4f3f9eac 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= merge_path_description(@merge_request, 'to') %>
-Author: <%= @merge_request.author_name %>
-Assignee: <%= @merge_request.assignee_name %>
+Author: <%= sanitize_name(@merge_request.author_name) %>
+Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 5acd45b74a7..db23447dd39 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -7,7 +7,7 @@
- if @merge_request.assignee_id.present?
%p
- Assignee: #{@merge_request.assignee_name}
+ Assignee: #{sanitize_name(@merge_request.assignee_name)}
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml
index 63b0cbbd205..d031842be95 100644
--- a/app/views/notify/new_ssh_key_email.html.haml
+++ b/app/views/notify/new_ssh_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new public key was added to your account:
%p
diff --git a/app/views/notify/new_ssh_key_email.text.erb b/app/views/notify/new_ssh_key_email.text.erb
index 05b551c89a0..690357d69ed 100644
--- a/app/views/notify/new_ssh_key_email.text.erb
+++ b/app/views/notify/new_ssh_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new public key was added to your account:
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index db4424a01f9..dfbb5c75bd3 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user['name']}!
+ Hi #{sanitize_name(@user['name'])}!
%p
- if Gitlab::CurrentSettings.allow_signup?
Your account has been created successfully.
diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb
index dd9b71e3b84..f3f20f3bfba 100644
--- a/app/views/notify/new_user_email.text.erb
+++ b/app/views/notify/new_user_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 294238eee51..722eedf90be 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 39622cf7f02..9aadf380f79 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
@@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %>
<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
index 67744ec1cee..97258833cfc 100644
--- a/app/views/notify/push_to_merge_request_email.html.haml
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -1,5 +1,5 @@
%h3
- = @updated_by_user.name
+ = sanitize_name(@updated_by_user.name)
pushed new commits to merge request
= link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
index 95759d127e2..10c8e158846 100644
--- a/app/views/notify/push_to_merge_request_email.text.haml
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference}
+#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference}
\
#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
\
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index ee2f40e1683..6d25488a7e2 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -2,7 +2,7 @@
Assignee changed
- if @previous_assignees.any?
from
- %strong= @previous_assignees.map(&:name).to_sentence
+ %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
to
- if @issue.assignees.any?
%strong= @issue.assignee_list
diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb
index 6c357f1074a..7bf2e8e6ce3 100644
--- a/app/views/notify/reassigned_issue_email.text.erb
+++ b/app/views/notify/reassigned_issue_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
+Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index 24c2b08810b..e4f19bc3200 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
- %strong= @previous_assignee.name
+ %strong= sanitize_name(@previous_assignee.name)
to
- if @merge_request.assignee_id
- %strong= @merge_request.assignee_name
+ %strong= sanitize_name(@merge_request.assignee_name)
- else
%strong Unassigned
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 998a40fefde..96c770b5219 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
- to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %>
+Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
+ to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
index 522421b7cc3..502b8f21e35 100644
--- a/app/views/notify/resolved_all_discussions_email.html.haml
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -1,2 +1,2 @@
%p
- All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name}
+ All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)}
diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb
index 2881f3e699e..c4b36bfe1a8 100644
--- a/app/views/notify/resolved_all_discussions_email.text.erb
+++ b/app/views/notify/resolved_all_discussions_email.text.erb
@@ -1,3 +1,3 @@
-All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %>
+All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= sanitize_name(@resolved_by.name) %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index e167e094240..ee2c5a13b8a 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,37 +1,37 @@
-- page_title "Account"
+- page_title _('Account')
- @content_class = "limit-container-width" unless fluid_layout
- if current_user.ldap_user?
.alert.alert-info
- Some options are unavailable for LDAP accounts
+ = s_('Profiles|Some options are unavailable for LDAP accounts')
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Two-Factor Authentication
+ = s_('Profiles|Two-Factor Authentication')
%p
- Increase your account's security by enabling Two-Factor Authentication (2FA).
+ = s_("Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)")
.col-lg-8
%p
- Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
+ #{_('Status')}: #{current_user.two_factor_enabled? ? _('Enabled') : _('Disabled')}
- if current_user.two_factor_enabled?
- = link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info'
+ = link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-info'
- else
.append-bottom-10
- = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success'
+ = link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-success'
%hr
- if display_providers_on_profile?
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Social sign-in
+ = s_('Profiles|Social sign-in')
%p
- Activate signin with one of the following services
+ = s_('Profiles|Activate signin with one of the following services')
.col-lg-8
%label.label-bold
- Connected Accounts
- %p Click on icon to activate signin with one of the following services
+ = s_('Profiles|Connected Accounts')
+ %p= s_('Profiles|Click on icon to activate signin with one of the following services')
- button_based_providers.each do |provider|
.provider-btn-group
.provider-btn-image
@@ -39,24 +39,24 @@
- if auth_active?(provider)
- if unlink_allowed?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
- Disconnect
+ = s_('Profiles|Disconnect')
- else
%a.provider-btn
- Active
+ = s_('Profiles|Active')
- else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
- Connect
+ = s_('Profiles|Connect')
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities]
%hr
- if current_user.can_change_username?
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.warning-title
- Change username
+ = s_('Profiles|Change username')
%p
- Changing your username can have unintended side effects.
+ = s_('Profiles|Changing your username can have unintended side effects.')
= succeed '.' do
- = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
+ = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
- data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) }
#update-username{ data: data }
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
index 23ef31a0c85..2bf514d72a5 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -23,9 +23,3 @@
%strong Signed in
on
= l(active_session.created_at, format: :short)
-
- - unless is_current_session
- .float-right
- = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
- %span.sr-only Revoke
- Revoke
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index d265f3c44ba..4b84835429c 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -1,12 +1,13 @@
-- page_title "New Password"
-- header_title "New Password"
-%h3.page-title Set up new password
+- page_title _('New Password')
+- breadcrumb_title _('New Password')
+
+%h3.page-title= _('Set up new password')
%hr
= form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
- Please set a new password before proceeding.
+ = _('Please set a new password before proceeding.')
%br
- After a successful password update you will be redirected to login screen.
+ = _('After a successful password update you will be redirected to login screen.')
= form_errors(@user)
@@ -22,4 +23,4 @@
.col-sm-10
= f.password_field :password_confirmation, required: true, class: 'form-control'
.form-actions
- = f.submit 'Set new password', class: "btn btn-success"
+ = f.submit _('Set new password'), class: 'btn btn-success'
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 7c378633667..bfe1c3ddf33 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -51,11 +51,36 @@
= f.label :dashboard, class: 'label-bold' do
Default dashboard
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+
+ = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
+
.form-group
= f.label :project_view, class: 'label-bold' do
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.form-text.text-muted
Choose what content you want to see on a project’s overview page.
+
+ .col-sm-12
+ %hr
+
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = _('Localization')
+ %p
+ = _('Customize language and region related settings.')
+ = succeed '.' do
+ = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank'
+ .col-lg-8
+ .form-group
+ = f.label :preferred_language, class: 'label-bold' do
+ = _('Language')
+ = f.select :preferred_language, language_choices, {}, class: 'select2'
+ .form-text.text-muted
+ = s_('Preferences|This feature is experimental and translations are not complete yet')
+ .form-group
+ = f.label :first_day_of_week, class: 'label-bold' do
+ = _('First day of the week')
+ = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control'
.form-group
- = f.submit 'Save changes', class: 'btn btn-success'
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 2629b374e7c..4d3d92d09c0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -2,7 +2,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
-= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
+= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@user)
.row
@@ -77,10 +77,10 @@
.col-lg-8
.row
- if @user.read_only_attribute?(:name)
- = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
+ = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9 qa-full-name' },
help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
- else
- = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
+ = f.text_field :name, label: 'Full name', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email)
@@ -95,9 +95,6 @@
= f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)),
{ help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } },
control_class: 'select2 input-lg'
- = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
- { help: s_("Profiles|This feature is experimental and translations are not complete yet") },
- control_class: 'select2 input-lg'
= f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
= f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
= f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 6bf21570d41..12da62f4c64 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,8 +1,8 @@
%div{ class: container_class }
- .nav-block.activity-filter-block.activities
+ .nav-block.d-none.d-sm-flex.activities
= render 'shared/event_filter'
.controls
- = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do
+ = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn d-none d-sm-inline-block has-tooltip' do
= icon('rss')
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index aa980da7e95..409b6dba9ca 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -19,7 +19,7 @@
%ul
%li Project and wiki repositories
%li Project uploads
- %li Project configuration including web hooks and services
+ %li Project configuration, including services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%li LFS objects
%p
@@ -28,13 +28,14 @@
%li Job traces and artifacts
%li Container registry images
%li CI variables
+ %li Webhooks
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- if project.export_status == :finished
- = link_to 'Download export', download_export_project_path(project),
+ = link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
- = link_to 'Generate new export', generate_new_export_project_path(project),
+ = link_to 'Generate new export', generate_new_export_project_path(project),
method: :post, class: "btn btn-default"
- else
= link_to 'Export project', export_project_path(project),
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 82b2ab64a5d..1d7287410ea 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,17 +1,18 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
+- max_project_topic_length = 15
.project-home-panel{ class: ("empty-project" if empty_repo) }
- .project-header.row.append-bottom-8
- .project-title-row.col-md-12.col-lg-6.d-flex
- .avatar-container.project-avatar.float-none
+ .row.append-bottom-8
+ .home-panel-title-row.col-md-12.col-lg-6.d-flex
+ .avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.project-title.qa-project-name
+ %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name
= @project.name
- %span.project-visibility.prepend-left-8.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
- .project-metadata.d-flex.align-items-center
+ .home-panel-metadata.d-flex.align-items-center.text-secondary
- if can?(current_user, :read_project, @project)
%span.text-secondary
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -19,16 +20,28 @@
%span.access-request-links.prepend-left-8
= render 'shared/members/access_request_links', source: @project
- if @project.tag_list.present?
- %span.project-tag-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil }
+ %span.home-panel-topic-list.d-inline-flex.prepend-left-8
= sprite_icon('tag', size: 16, css_class: 'icon append-right-4')
- = @project.tags_to_show
- - if @project.has_extra_tags?
- = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
+
+ - @project.topics_to_show.each do |topic|
+ - project_topics_classes = "badge badge-pill badge-secondary append-right-5"
+ - explore_project_topic_path = explore_projects_path(tag: topic)
+ - if topic.length > max_project_topic_length
+ %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path }
+ = topic.titleize
+ - else
+ %a{ class: project_topics_classes, href: explore_project_topic_path }
+ = topic.titleize
+
+ - if @project.has_extra_topics?
+ .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil }
+ = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown }
+
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
- = render 'projects/buttons/notifications', notification_setting: @notification_setting, btn_class: 'btn-xs'
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs'
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
@@ -43,13 +56,13 @@
- if can?(current_user, :download_code, @project)
%nav.project-stats
- .nav-links.quick-links.mt-3
+ .nav-links.quick-links
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- .project-home-desc.mt-1
+ .home-panel-home-desc.mt-1
- if @project.description.present?
- .project-description
- .project-description-markdown.read-more-container
+ .home-panel-description
+ .home-panel-description-markdown.read-more-container
= markdown_field(@project, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more")
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index d59191a6f87..0b2d179456d 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -21,7 +21,7 @@
= clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
- if issuable_type == 'issue'
- - enter_title_text = _('Enter the issue title')
+ - enter_title_text = _('Enter the issue title')
- enter_description_text = _('Enter the issue description')
- else
- enter_title_text = _('Enter the merge request title')
@@ -36,7 +36,12 @@
%p
= render 'by_email_description'
%p
- This is a private email address, generated just for you.
+ This is a private email address
+
+ %a{ href: 'https://docs.gitlab.com/ee/development/emails.html#email-namespace', target: "_blank", rel: "noopener" }
+ %i.fa.fa-question-circle{ 'aria-label': "Learn more about incoming email addresses" }
+
+ generated just for you.
Anyone who gets ahold of it can create issues or merge requests as if they were you.
You should
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 0f709c65d0e..830cfa80d58 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -12,23 +12,13 @@
%ul.nav.nav-tabs.nav-links.clearfix
%li.md-header-tab.active
%button.js-md-write-button{ tabindex: -1 }
- Write
+ = _("Write")
%li.md-header-tab
%button.js-md-preview-button{ tabindex: -1 }
- Preview
+ = _("Preview")
%li.md-header-toolbar.active
- = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
- = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
- = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
- = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
- = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
- = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
- = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
- = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
- = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
- %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
- = sprite_icon("screen-full")
+ = render 'projects/blob/markdown_buttons', show_fullscreen_button: true
.md-write-holder
= yield
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index f178c94e008..6ac2e06afa5 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -9,6 +9,7 @@
%span.descr
Pipelines need to be configured to enable this feature.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
+ = render_if_exists 'projects/merge_pipelines_settings', form: form
.form-check
= form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
= form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index ba7d3154326..276363df7da 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -43,7 +43,7 @@
= f.label :description, class: 'label-bold' do
Project description
%span (optional)
- = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
+ = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
= f.label :visibility_level, class: 'label-bold' do
Visibility Level
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index 45e1d32980c..de4653dad2c 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -2,7 +2,7 @@
%div{ class: container_class }
.prepend-top-default.append-bottom-default
.wiki
- = render_wiki_content(@wiki_home, legacy_render_context(params))
+ = render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.landing{ class: [('row-content-block row p-0 align-items-center' if can_create_wiki), ('content-block' unless can_create_wiki)] }
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index cfb91568061..f42d5128715 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -14,4 +14,4 @@
= link_to path_to_file, class: 'str-truncated' do
%span= blob.name
%td
- = number_to_human_size(blob.size, precision: 2)
+ = number_to_human_size(blob.size)
diff --git a/app/views/projects/badges/badge_flat-square.svg.erb b/app/views/projects/badges/badge_flat-square.svg.erb
new file mode 100644
index 00000000000..5b90da15ef5
--- /dev/null
+++ b/app/views/projects/badges/badge_flat-square.svg.erb
@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= badge.width %>" height="20">
+ <g shape-rendering="crispEdges">
+ <path fill="<%= badge.key_color %>" d="M0 0 h<%= badge.key_width %> v20 H0 z"/>
+ <path fill="<%= badge.value_color %>" d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/>
+ </g>
+
+ <g fill="#fff" text-anchor="middle">
+ <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
+ <text x="<%= badge.key_text_anchor %>" y="14">
+ <%= badge.key_text %>
+ </text>
+ <text x="<%= badge.value_text_anchor %>" y="14">
+ <%= badge.value_text %>
+ </text>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 3c1f33ea95e..a54460f1196 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,4 +1,6 @@
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
+- file_name = params[:id].split("/").last ||= ""
+- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix{ data: { current_action: action } }
@@ -17,6 +19,8 @@
required: true, class: 'form-control new-file-name js-file-path-name-input'
.file-buttons
+ - if is_markdown
+ = render 'projects/blob/markdown_buttons', show_fullscreen_button: false
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/blob/_markdown_buttons.html.haml b/app/views/projects/blob/_markdown_buttons.html.haml
new file mode 100644
index 00000000000..28d1ff97825
--- /dev/null
+++ b/app/views/projects/blob/_markdown_buttons.html.haml
@@ -0,0 +1,13 @@
+.md-header-toolbar.active
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: _("Add bold text") })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: _("Add italic text") })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") })
+ = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: _("Add a bullet list") })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: _("Add a task list") })
+ = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
+ - if show_fullscreen_button
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
+ = sprite_icon("screen-full")
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 3f2d96b70e5..4520cca8cf5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -21,7 +21,7 @@
Write
%li
- = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do
+ = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
= editing_preview_title(@blob.name)
= form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths(@project)) do
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index ff460a3831c..66687f087ff 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -2,7 +2,7 @@
.diff-content
- if markup?(@blob.name)
.file-content.wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
- = markup(@blob.name, @content, legacy_render_context(params))
+ = markup(@blob.name, @content)
- else
.file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
index 87aa7c1dbf8..5970d41fdab 100644
--- a/app/views/projects/blob/viewers/_dependency_manager.html.haml
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -3,9 +3,4 @@
This project manages its dependencies using
%strong= viewer.manager_name
- - if viewer.package_name
- and defines a #{viewer.package_type} named
- %strong<
- = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
-
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
index 5be7cc7f25a..61d67a88a5a 100644
--- a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
@@ -1,9 +1,9 @@
-- if viewer.valid?(@project, @commit.sha)
+- if viewer.valid?(project: @project, sha: @commit.sha, user: @current_user)
= icon('check fw')
This GitLab CI configuration is valid.
- else
= icon('warning fw')
This GitLab CI configuration is invalid:
- = viewer.validation_message(@project, @commit.sha)
+ = viewer.validation_message(project: @project, sha: @commit.sha, user: @current_user)
= link_to 'Learn more', help_page_path('ci/yaml/README')
diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml
index 120c0540335..df1f3e4e01b 100644
--- a/app/views/projects/blob/viewers/_loading.html.haml
+++ b/app/views/projects/blob/viewers/_loading.html.haml
@@ -1,2 +1,2 @@
.text-center.prepend-top-default.append-bottom-default
- = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…')
+ = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…', class: 'qa-spinner')
diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml
index 6edbfd91b21..1a77eb078be 100644
--- a/app/views/projects/blob/viewers/_markup.html.haml
+++ b/app/views/projects/blob/viewers/_markup.html.haml
@@ -1,6 +1,4 @@
- blob = viewer.blob
-- context = legacy_render_context(params)
-- unless context[:markdown_engine] == :redcarpet
- - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup)
+- context = blob.respond_to?(:rendered_markup) ? { rendered: blob.rendered_markup } : {}
.file-content.wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
= markup(blob.name, blob.data, context)
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index d8492abc638..c2329a7aa66 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= icon('info-circle fw')
= succeed '.' do
To learn more about this project, read
- = link_to "the wiki", get_project_wiki_path(viewer.project)
+ = link_to "the wiki", project_wiki_path(viewer.project, :home)
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 88f9b7dfc9f..91c51d5e091 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -2,6 +2,7 @@
- commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
+- number_commits_distance = diverging_commit_counts[:distance]
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = merge_request_source_project_for_project(@project)
@@ -28,16 +29,23 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
- default_branch: @repository.root_ref,
- number_commits_ahead: diverging_count_label(number_commits_ahead) } }
- .graph-side
- .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
- %span.count.count-behind= diverging_count_label(number_commits_behind)
- .graph-separator
- .graph-side
- .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
- %span.count.count-ahead= diverging_count_label(number_commits_ahead)
+ - if number_commits_distance.nil?
+ .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ default_branch: @repository.root_ref,
+ number_commits_ahead: diverging_count_label(number_commits_ahead) } }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= diverging_count_label(number_commits_behind)
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= diverging_count_label(number_commits_ahead)
+ - else
+ .divergence-graph.d-none.d-md-block{ title: s_('More than %{number_commits_distance} commits different with %{default_branch}') % { number_commits_distance: diverging_count_label(number_commits_distance),
+ default_branch: @repository.root_ref} }
+ .graph-side.full
+ .bar{ style: "width: #{number_commits_distance * bar_graph_width_factor}%" }
+ %span.count= diverging_count_label(number_commits_distance)
.controls.d-none.d-md-block<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
@@ -45,9 +53,8 @@
= _('Merge request')
- if branch.name != @repository.root_ref
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
+ = link_to project_compare_path(@project, @repository.root_ref, branch.name),
class: "btn btn-default #{'prepend-left-10' unless merge_project}",
- method: :post,
title: s_('Branches|Compare') do
= s_('Branches|Compare')
@@ -76,7 +83,7 @@
= icon("trash-o")
- else
= link_to project_branch_path(@project, branch.name),
- class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip",
title: s_('Branches|Delete branch'),
method: :delete,
data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 0e4b119bb54..93061452e12 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -10,7 +10,7 @@
.card.prepend-top-10
.card-header
= panel_title
- %ul.content-list.all-branches
+ %ul.content-list.all-branches.qa-all-branches
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch)
- if branches.size > overview_max_branches
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index ca867961f6b..43f1cd01b67 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -35,7 +35,7 @@
- if can? current_user, :push_code, @project
= link_to project_merged_branches_path(@project),
- class: 'btn btn-inverted btn-remove has-tooltip',
+ class: 'btn btn-inverted btn-remove has-tooltip qa-delete-merged-branches',
title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref },
method: :delete,
data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'),
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 53d427ec40a..09f05b30433 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -5,18 +5,18 @@
%span.append-right-4.js-clone-dropdown-label
= _('Clone')
= sprite_icon("arrow-down", css_class: "icon")
- %form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
- - if ssh_enabled?
- %li.pb-2
- %label.label-bold
- = _('Clone with SSH')
- .input-group
- = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
- .input-group-append
- = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
- = render_if_exists 'projects/buttons/geo'
- %li
- - if http_enabled?
+ %ul.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
+ - if ssh_enabled?
+ %li
+ %label.label-bold
+ = _('Clone with SSH')
+ .input-group
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
+ .input-group-append
+ = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+ = render_if_exists 'projects/buttons/geo'
+ - if http_enabled?
+ %li.pt-2
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group
@@ -24,5 +24,6 @@
.input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
+ = render_if_exists 'projects/buttons/kerberos_clone_field'
= render_if_exists 'shared/geo_info_modal', project: project
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 45515fb492f..bbe0a2c97fd 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -35,8 +35,8 @@
- elsif can_collaborate_with_project?(@project)
%li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
- elsif create_mr_from_new_fork
- - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
- notice: edit_in_new_fork_notice,
+ - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
+ notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
%li= link_to _('New file'), fork_path, method: :post
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
deleted file mode 100644
index 745983ace7e..00000000000
--- a/app/views/projects/buttons/_notifications.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- btn_class = local_assigns.fetch(:btn_class, "btn-xs")
-
-- if notification_setting
- .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
- = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
- = hidden_setting_source_input(notification_setting)
- = hidden_field_tag "hide_label", true
- = f.hidden_field :level, class: "notification_setting_level"
- .js-notification-toggle-btns
- %div{ class: ("btn-group" if notification_setting.custom?) }
- - if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
- = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
- %span.js-notification-loading.fa.hidden
- %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
- = sprite_icon("arrow-down", css_class: "icon")
- .sr-only Toggle dropdown
- - else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
- = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
- %span.js-notification-loading.fa.hidden
- = sprite_icon("arrow-down", css_class: "icon")
-
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
-
- = content_for :scripts_body do
- = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 44e9cb84341..9d069c025ba 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -25,7 +25,7 @@
= job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
- .light none
+ .light= _('none')
.icon-container.commit-icon
= custom_icon("icon_commit")
@@ -33,10 +33,10 @@
= link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha"
- if job.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
+ = icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
+ = icon('refresh', class: 'text-warning has-tooltip', title: _('Job was retried'))
.label-container
- if job.tags.any?
@@ -44,13 +44,13 @@
%span.badge.badge-primary
= tag
- if job.try(:trigger_request)
- %span.badge.badge-info triggered
+ %span.badge.badge-info= _('triggered')
- if job.try(:allow_failure)
- %span.badge.badge-danger allowed to fail
+ %span.badge.badge-danger= _('allowed to fail')
- if job.schedulable?
%span.badge.badge-info= s_('DelayedJobs|delayed')
- elsif job.action?
- %span.badge.badge-info manual
+ %span.badge.badge-info= _('manual')
- if pipeline_link
%td
@@ -70,7 +70,7 @@
- if job.try(:runner)
= runner_link(job.runner)
- else
- .light none
+ .light= _('none')
- if stage
%td
@@ -97,11 +97,11 @@
%td
.float-right
- if can?(current_user, :read_build, job) && job.artifacts?
- = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
+ = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build' do
= sprite_icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
- = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: 'Cancel', class: 'btn btn-build' do
+ = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif job.scheduled?
.btn-group
@@ -123,8 +123,8 @@
= sprite_icon('time-out')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
- = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
+ = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn btn-build' do
= custom_icon('icon_play')
- elsif job.retryable?
- = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
= icon('repeat')
diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index 30bf1384b22..59b5b9f8a30 100644
--- a/app/views/projects/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
@@ -1,45 +1,48 @@
- if @status
%p
- %b Status:
- syntax is correct
+ %b= _("Status:")
+ = _("syntax is correct")
%i.fa.fa-ok.correct-syntax
.table-holder
%table.table.table-bordered
%thead
%tr
- %th Parameter
- %th Value
+ %th= _("Parameter")
+ %th= _("Value")
%tbody
- @stages.each do |stage|
- @builds.select { |build| build[:stage] == stage }.each do |build|
+ - job = @jobs[build[:name].to_sym]
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
- %pre= build[:commands]
+ %pre= job[:before_script].to_a.join('\n')
+ %pre= job[:script].to_a.join('\n')
+ %pre= job[:after_script].to_a.join('\n')
%br
- %b Tag list:
+ %b= _("Tag list:")
= build[:tag_list].to_a.join(", ")
%br
- %b Only policy:
- = @jobs[build[:name].to_sym][:only].to_a.join(", ")
+ %b= _("Only policy:")
+ = job[:only].to_a.join(", ")
%br
- %b Except policy:
- = @jobs[build[:name].to_sym][:except].to_a.join(", ")
+ %b= _("Except policy:")
+ = job[:except].to_a.join(", ")
%br
- %b Environment:
+ %b= _("Environment:")
= build[:environment]
%br
- %b When:
+ %b= _("When:")
= build[:when]
- if build[:allow_failure]
- %b Allowed to fail
+ %b= _("Allowed to fail")
- else
%p
- %b Status:
- syntax is incorrect
+ %b= _("Status:")
+ = _("syntax is incorrect")
%i.fa.fa-remove.incorrect-syntax
- %b Error:
+ %b= _("Error:")
= @error
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index cbda6bf2107..7b87664961e 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -1,9 +1,9 @@
-- page_title "CI Lint"
-- page_description "Validate your GitLab CI configuration file"
+- page_title _("CI Lint")
+- page_description _("Validate your GitLab CI configuration file")
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2.pt-3.pb-3 Check your .gitlab-ci.yml
+%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml")
.project-ci-linter
= form_tag project_ci_lint_path(@project), method: :post do
@@ -11,14 +11,14 @@
.col-sm-12
.file-holder
.js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
+ = _("Contents of .gitlab-ci.yml")
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.float-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ = submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
.float-right.prepend-top-10
- = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
+ = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20
.col-sm-12
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index cecc139b183..888be4ee282 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -24,6 +24,6 @@
= _("No file selected")
= f.file_field :bfg_object_map, accept: 'text/plain', class: "hidden js-object-map-input", required: true
.form-text.text-muted
- = _("The maximum file size allowed is %{max_attachment_size}mb") % { max_attachment_size: Gitlab::CurrentSettings.max_attachment_size }
+ = _("The maximum file size allowed is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
= f.submit _('Start cleanup'), class: 'btn btn-success'
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index eb677cff5f0..ae9aef5a9b0 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f6666921a25..41f5fb3dcbd 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,10 +1,12 @@
+- any_pipelines = @commit.present(current_user: current_user).any_pipelines?
+
%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
= nav_link(path: 'commit#show') do
= link_to project_commit_path(@project, @commit.id) do
- Changes
+ = _('Changes')
%span.badge.badge-pill= @diffs.size
- - if can?(current_user, :read_pipeline, @project)
+ - if any_pipelines
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_project_commit_path(@project, @commit.id) do
- Pipelines
+ = _('Pipelines')
%span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 2a919a767c0..a0db48bf8ff 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -6,8 +6,8 @@
%strong
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
- = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard"))
- %span.d-none.d-sm-inline authored
+ = clipboard_button(text: @commit.id, title: _('Copy commit SHA to clipboard'))
+ %span.d-none.d-sm-inline= _('authored')
#{time_ago_with_tooltip(@commit.authored_date)}
%span= s_('ByAuthor|by')
= author_avatar(@commit, size: 24, has_tooltip: false)
@@ -43,13 +43,13 @@
= cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
- if can?(current_user, :push_code, @project)
%li.clearfix
- = link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit)
+ = link_to s_('CreateTag|Tag'), new_project_tag_path(@project, ref: @commit)
%li.divider
%li.dropdown-header
#{ _('Download') }
- unless @commit.parents.length > 1
- %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches"
- %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff"
+ %li= link_to s_('DownloadCommit|Email Patches'), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches"
+ %li= link_to s_('DownloadCommit|Plain Diff'), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff"
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
@@ -74,8 +74,8 @@
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
= icon('spinner spin')
- - if @commit.last_pipeline
- - last_pipeline = @commit.last_pipeline
+ - last_pipeline = @commit.last_pipeline
+ - if can?(current_user, :read_pipeline, last_pipeline)
.well-segment.pipeline-info
.status-icon-container
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
@@ -95,8 +95,5 @@
.well-segment
= icon('info-circle fw')
- This commit is part of merge request
- = succeed '.' do
- = link_to @merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id)
-
- Comments created here will be created in the context of that merge request.
+ - link_to_merge_request = link_to(@merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id))
+ = _('This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request.').html_safe % { link_to_merge_request: link_to_merge_request }
diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml
index a264f3517c4..7d3c0582d0b 100644
--- a/app/views/projects/commit/_limit_exceeded_message.html.haml
+++ b/app/views/projects/commit/_limit_exceeded_message.html.haml
@@ -1,8 +1,8 @@
-.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} }
+.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: _('Project has too many %{label_for_message} to search') % { label_for_message: label_for_message } } }
.limit-icon
- if objects == :branch
= sprite_icon('fork', size: 12)
- else
= icon('tag')
.limit-message
- %span #{label_for_message.capitalize} unavailable
+ %span= _('%{label_for_message} unavailable') % { label_for_message: label_for_message.capitalize }
diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml
index d7bf2dc0cb6..bb843bee7c9 100644
--- a/app/views/projects/commit/_other_user_signature_badge.html.haml
+++ b/app/views/projects/commit/_other_user_signature_badge.html.haml
@@ -1,6 +1,6 @@
- title = capture do
- This commit was signed with a different user's verified signature.
+ = _("This commit was signed with a different user's verified signature.")
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
index 22ffd66ff8e..d282ab4f520 100644
--- a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
+++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
@@ -1,7 +1,6 @@
- title = capture do
- This commit was signed with a verified signature, but the committer email
- is <strong>not verified</strong> to belong to the same user.
+ = _('This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user.').html_safe
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: _('Unverified'), css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index c4d986ef742..1331fa179fc 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -19,10 +19,10 @@
.clearfix
= render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
- GPG Key ID:
+ = _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
- = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml
index 00e1efe0582..294f916d18f 100644
--- a/app/views/projects/commit/_unverified_signature_badge.html.haml
+++ b/app/views/projects/commit/_unverified_signature_badge.html.haml
@@ -1,6 +1,6 @@
- title = capture do
- This commit was signed with an <strong>unverified</strong> signature.
+ = _('This commit was signed with an <strong>unverified</strong> signature.').html_safe
-- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless' }
+- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml
index 31408806be7..4964b1b8ee7 100644
--- a/app/views/projects/commit/_verified_signature_badge.html.haml
+++ b/app/views/projects/commit/_verified_signature_badge.html.haml
@@ -1,7 +1,6 @@
- title = capture do
- This commit was signed with a <strong>verified</strong> signature and the
- committer email is verified to belong to the same user.
+ = _('This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.').html_safe
-- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'status_success_borderless', show_user: true }
+- locals = { signature: signature, title: title, label: _('Verified'), css_class: 'valid', icon: 'status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index c66ea873dba..f8c27f4c026 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Pipelines', "#{@commit.title} (#{@commit.short_id})", 'Commits'
+- page_title _('Pipelines'), "#{@commit.title} (#{@commit.short_id})", _('Commits')
= render 'commit_box'
= render 'ci_menu'
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 79e32949db9..34226167288 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,19 +1,16 @@
- @no_container = true
-- add_to_breadcrumbs "Commits", project_commits_path(@project)
+- add_to_breadcrumbs _('Commits'), project_commits_path(@project)
- breadcrumb_title @commit.short_id
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout ? '' : 'limit-container-width'
- @content_class = limited_container_width
-- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title "#{@commit.title} (#{@commit.short_id})", _('Commits')
- page_description @commit.description
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
- - if @commit.status
- = render "ci_menu"
- - else
- .block-connector
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
+ = render "ci_menu"
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-commit"
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1a74b120c26..0d3c6e7027c 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,6 +6,7 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- commit_status = commit.present(current_user: current_user).status_for(ref)
- link = commit_path(project, commit, merge_request: merge_request)
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -22,7 +23,7 @@
%span.commit-row-message.d-block.d-sm-none
&middot;
= commit.short_id
- - if commit.status(ref)
+ - if commit_status
.d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
@@ -45,7 +46,7 @@
- else
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
- - if commit.status(ref)
+ - if commit_status
= render_commit_status(commit, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index b6bebbabed0..5774b48a054 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-compare"
- else
.card.bg-light
.center
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 062aa423bde..24d665761cc 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%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.
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index f4c91377ecb..e45c5be76de 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -11,4 +11,4 @@
- next unless can?(current_user, :update_build, action)
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do
- %span= action.name.humanize
+ %span= action.name
diff --git a/app/views/projects/deployments/_confirm_rollback_modal.html.haml b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
new file mode 100644
index 00000000000..ff40e404e5f
--- /dev/null
+++ b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
@@ -0,0 +1,23 @@
+- commit_sha = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha has-tooltip", title: h(deployment.commit_title)
+.modal.ws-normal.fade{ tabindex: -1, id: "confirm-rollback-modal-#{deployment.id}" }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %h4.modal-title.d-flex.mw-100
+ - if deployment.last?
+ = s_("Environments|Re-deploy environment %{environment_name}?") % {environment_name: @environment.name}
+ - else
+ = s_("Environments|Rollback environment %{environment_name}?") % {environment_name: @environment.name}
+ .modal-body
+ - if deployment.last?
+ %p= s_('Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
+ - else
+ %p
+ = s_('Environments|This action will run the job defined by staging for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
+ .modal-footer
+ = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
+ = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-danger' do
+ - if deployment.last?
+ = s_('Environments|Re-deploy')
+ - else
+ = s_('Environments|Rollback')
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 1bd538a08ff..d6bf8d564de 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,7 +1,8 @@
- if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
- = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
+ = button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
- if deployment.last?
= sprite_icon('repeat')
- else
= sprite_icon('redo')
+ = render 'projects/deployments/confirm_rollback_modal', deployment: deployment
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index cc2d0d3b2d8..2dba3fcd664 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -2,7 +2,7 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
-- is_commit = local_assigns.fetch(:is_commit, false)
+- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
@@ -25,4 +25,4 @@
= render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, is_commit: is_commit }
+ = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 5565ae1d98b..855b719dc45 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,11 +1,11 @@
- environment = local_assigns.fetch(:environment, nil)
-- is_commit = local_assigns.fetch(:is_commit, false)
+- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
- file_hash = hexdigest(diff_file.file_path)
- image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image'
- image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) }
- .js-file-title.file-title-flex-parent{ class: is_commit ? "is-commit" : "" }
+ .js-file-title.file-title-flex-parent{ class: diff_page_context }
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}"
diff --git a/app/views/projects/diffs/_render_error.html.haml b/app/views/projects/diffs/_render_error.html.haml
index c3dc47a56a7..7dbd9897e83 100644
--- a/app/views/projects/diffs/_render_error.html.haml
+++ b/app/views/projects/diffs/_render_error.html.haml
@@ -1,6 +1,2 @@
.nothing-here-block
- = _("This %{viewer} could not be displayed because %{reason}.") % { viewer: viewer.switcher_title, reason: diff_render_error_reason(viewer) }
-
- You can
- = diff_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
- instead.
+ = viewer.render_error_message.html_safe
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index 2eef599cf84..2cc3d921abc 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -9,4 +9,4 @@
= link_to _("Plain diff"), merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
= link_to _("Email patch"), merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
- = _("To preserve performance only <strong>%{display_size} of ${real_size}</strong> files are displayed.").html_safe % { display_size: diff_files.size, real_size: diff_files.real_size }
+ = _("To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed.").html_safe % { display_size: diff_files.size, real_size: diff_files.real_size }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 1b52821af15..a6a8ca489a9 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,17 +1,15 @@
-- breadcrumb_title "General Settings"
-- page_title "General"
+- breadcrumb_title _("General Settings")
+- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test?
.project-edit-container
%section.settings.general-settings.no-animate#js-general-project-settings{ class: ('expanded' if expanded) }
.settings-header
- %h4
- General project
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- Update your project name, description, avatar, and other general settings.
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, tags, avatar')
+ %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ %p= _('Update your project name, tags, description and avatar.')
+
.settings-content
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
@@ -39,14 +37,14 @@
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
.form-group
- = f.label :tag_list, "Tags", class: 'label-bold'
+ = f.label :tag_list, "Topics", class: 'label-bold'
= f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
- %p.form-text.text-muted Separate tags with commas.
+ %p.form-text.text-muted Separate topics with commas.
%fieldset.features
%h5.prepend-top-0= _("Project avatar")
.form-group
- if @project.avatar?
- .avatar-container.s160.append-bottom-15
+ .avatar-container.rect-avatar.s160.append-bottom-15
= project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160)
- if @project.avatar_in_git
%p.light
@@ -63,12 +61,10 @@
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) }
.settings-header
- %h4
- Permissions
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- Enable or disable certain project features and choose access levels.
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
+ %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.')
+
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
@@ -81,12 +77,10 @@
%section.qa-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
- %h4
- Merge request
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- Customize your merge request restrictions.
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
+ %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ %p= _('Choose your merge method, set up a default merge request description template.')
+
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
@@ -97,11 +91,10 @@
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
- = render_if_exists 'projects/service_desk_settings'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
- %h4
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('ProjectSettings|Badges')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
@@ -111,16 +104,15 @@
.settings-content
= render 'shared/badges/badge_settings'
+ = render_if_exists 'projects/service_desk_settings'
= render 'export', project: @project
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
- %h4
- Advanced
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
+ %button.btn.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
+ %p= _('Housekeeping, export, path, transfer, remove, archive.')
+
.settings-content
.sub-section
%h4 Housekeeping
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index b7e1cf85cb7..aebd176af9b 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,8 +1,5 @@
- @no_container = true
- page_title _("Environments")
-#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
- "folder-name" => @folder,
- "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
- "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data,
"css-class" => container_class } }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index d66de7ab698..99cbbc11acd 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -2,7 +2,6 @@
- page_title _("Environments")
#environments-list-view{ data: { environments_data: environments_list_data,
- "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml
new file mode 100644
index 00000000000..bc02c5f0e5a
--- /dev/null
+++ b/app/views/projects/error_tracking/index.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Errors')
+
+#js-error_tracking{ data: error_tracking_data(@project) }
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
index a69146513d8..3f0798a898d 100644
--- a/app/views/projects/forks/_fork_button.html.haml
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -5,7 +5,7 @@
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
= link_to project_path(forked_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
- = group_icon(namespace, class: "avatar s100 identicon")
+ = group_icon(namespace, class: "avatar rect-avatar s100 identicon")
- else
.avatar-container.s100
= image_tag(avatar, class: "avatar s100")
@@ -18,7 +18,7 @@
class: ("disabled has-tooltip" unless can_create_project),
title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
- = group_icon(namespace, class: "avatar s100 identicon")
+ = group_icon(namespace, class: "avatar rect-avatar s100 identicon")
- else
.avatar-container.s100
= image_tag(avatar, class: "avatar s100")
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index b0e22a35fe1..60160f521ad 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -55,23 +55,23 @@
#{@commits_graph.authors}
= (_("Authors: %{authors}") % { authors: "<strong>#{authors}</strong>" }).html_safe
.col-md-6
+ %p.slead
+ = _("Commits per day of month")
%div
- %p.slead
- = _("Commits per day of month")
%canvas#month-chart
.row
.col-md-6
.col-md-6
+ %p.slead
+ = _("Commits per weekday")
%div
- %p.slead
- = _("Commits per weekday")
%canvas#weekday-chart
.row
.col-md-6
.col-md-6
+ %p.slead
+ = _("Commits per day hour (UTC)")
%div
- %p.slead
- = _("Commits per day hour (UTC)")
%canvas#hour-chart
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 4917f4b8903..42b6aaa2634 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -5,7 +5,7 @@
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-%section.js-vue-notes-event
+%section.issuable-discussion.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
noteable_data: serialize_issuable(@issue),
noteable_type: 'Issue',
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 1e4e9450ffa..1be1087b36f 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,4 +1,3 @@
= form_for [@project.namespace.becomes(Namespace), @project, @issue],
- html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' },
- data: { markdown_version: @issue.cached_markdown_version } do |f|
+ html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 31c72f2f759..ce7c7091c93 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -36,7 +36,7 @@
= issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- - issue.labels.each do |label|
+ - labels_sorted_by_title(issue.labels).each do |label|
= link_to_label(label, subject: issue.project, css_class: 'label-link')
.issuable-meta
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 5c36d2202a6..6a66c2e57cc 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,35 +1,36 @@
- if @merge_requests.any?
- %h2.merge-requests-title
- = pluralize(@merge_requests.count, 'Related Merge Request')
- %ul.unstyled-list.related-merge-requests
- - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- - @merge_requests.each do |merge_request|
- %li
- %span.merge-request-ci-status
- - if merge_request.head_pipeline
- = render_pipeline_status(merge_request.head_pipeline)
- - elsif has_any_head_pipeline
- = icon('blank fw')
- %span.merge-request-id
- = merge_request.to_reference
- %span.merge-request-info
- %strong
- = link_to merge_request.title, merge_request_path(merge_request), class: "row_title"
- - unless @issue.project.id == merge_request.target_project.id
- in
- - project = merge_request.target_project
- = link_to project.full_name, project_path(project)
+ .card-slim.mt-3
+ .card-header
+ %h2.card-title.mt-0.mb-0.h5.merge-requests-title
+ %span.mr-1.bold
+ = _('Related merge requests')
+ .d-inline-flex.lh-100.align-middle
+ .mr-count-badge
+ .mr-count-badge-count
+ = sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary')
+ = @merge_requests.count
+ %ul.content-list.related-items-list
+ - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
+ - @merge_requests.each do |merge_request|
+ - merge_request = merge_request.present(current_user: current_user)
+ %li.list-item.py-0.px-0
+ .item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
+ .item-contents
+ .item-title.d-flex.align-items-center.mr-title
+ = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' }
+ = link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'}
+ .item-meta
+ = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' }
+ %span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0
+ %span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path }
+ = merge_request.target_project.full_path
+ = merge_request.to_reference
+ %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
+ - if merge_request.can_read_pipeline?
+ = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom'
+ - elsif has_any_head_pipeline
+ = icon('blank fw')
- - if merge_request.merged?
- %span.merge-request-status.prepend-left-10.merged
- Merged
- - elsif merge_request.closed?
- %span.merge-request-status.prepend-left-10.closed
- Closed
- - else
- %span.merge-request-status.prepend-left-10.open
- Open
-
- - if @closed_by_merge_requests.present?
- %li
- = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
+ - if @closed_by_merge_requests.present?
+ %p
+ = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml
new file mode 100644
index 00000000000..90838a75214
--- /dev/null
+++ b/app/views/projects/issues/_merge_requests_status.html.haml
@@ -0,0 +1,25 @@
+- time_format = '%b %e, %Y %l:%M%P %Z%z'
+
+- if merge_request.merged?
+ - mr_status_date = merge_request.merged_at
+ - mr_status_title = _('Merged')
+ - mr_status_icon = 'merge'
+ - mr_status_class = 'merged'
+- elsif merge_request.closed?
+ - mr_status_date = merge_request.closed_event&.created_at
+ - mr_status_title = _('Closed')
+ - mr_status_icon = 'issue-close'
+ - mr_status_class = 'closed'
+- else
+ - mr_status_date = merge_request.created_at
+ - mr_status_title = mr_status_date ? _('Opened') : _('Open')
+ - mr_status_icon = 'issue-open-m'
+ - mr_status_class = 'open'
+
+- if mr_status_date
+ - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>"
+- else
+ - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>"
+
+%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } }
+ = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}")
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index e4a0d4b8479..329efa0cdbf 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,11 +1,30 @@
-= render 'shared/issuable/feed_buttons'
-
-- if @can_bulk_update
- = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
-- if show_new_issue_link?(@project)
- = link_to "New issue", new_project_issue_path(@project,
- issue: { assignee_id: finder.assignee.try(:id),
- milestone_id: finder.milestones.first.try(:id) }),
- class: "btn btn-success",
- title: "New issue",
- id: "new_issue_link"
+- show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true)
+- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project)
+- show_export_button = local_assigns.fetch(:show_export_button, true)
+
+.nav-controls.issues-nav-controls
+ - if show_feed_buttons
+ = render 'shared/issuable/feed_buttons'
+
+ .btn-group.append-right-10<
+ - if show_export_button
+ = render_if_exists 'projects/issues/export_csv/button'
+
+ - if show_import_button
+ = render 'projects/issues/import_csv/button'
+
+ - if @can_bulk_update
+ = button_tag _("Edit issues"), class: "btn btn-default append-right-10 js-bulk-update-toggle"
+ - if show_new_issue_link?(@project)
+ = link_to _("New issue"), new_project_issue_path(@project,
+ issue: { assignee_id: finder.assignee.try(:id),
+ milestone_id: finder.milestones.first.try(:id) }),
+ class: "btn btn-success",
+ title: _("New issue"),
+ id: "new_issue_link"
+
+- if show_export_button
+ = render_if_exists 'projects/issues/export_csv/modal'
+
+- if show_import_button
+ = render 'projects/issues/import_csv/modal'
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 5374f4a1de0..fbd70cd1906 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -30,7 +30,7 @@
= icon('check', class: 'icon')
= _('Create merge request and branch')
- %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
+ %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
.menu-item
= icon('check', class: 'icon')
= _('Create branch')
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 1df38db9fd4..6da4956a036 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -6,9 +6,9 @@
%li
- target = @project.repository.find_branch(branch).dereferenced_target
- pipeline = @project.pipeline_for(branch, target.sha) if target
- - if pipeline
+ - if can?(current_user, :read_pipeline, pipeline)
%span.related-branch-ci-status
- = render_pipeline_status(pipeline)
+ = render 'ci/status/icon', status: pipeline.detailed_status(current_user)
%span.related-branch-info
%strong
= link_to branch, project_compare_path(@project, from: @project.default_branch, to: branch), class: "ref-name"
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
new file mode 100644
index 00000000000..acc2c50294f
--- /dev/null
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -0,0 +1,9 @@
+- type = local_assigns.fetch(:type, :icon)
+
+%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
+ data: { toggle: 'modal', target: '.issues-import-modal' } }
+ - if type == :icon
+ = sprite_icon('upload')
+ - else
+ = _('Import CSV')
+
diff --git a/app/views/projects/issues/import_csv/_modal.html.haml b/app/views/projects/issues/import_csv/_modal.html.haml
new file mode 100644
index 00000000000..86bc54786ad
--- /dev/null
+++ b/app/views/projects/issues/import_csv/_modal.html.haml
@@ -0,0 +1,24 @@
+.issues-import-modal.modal
+ .modal-dialog
+ .modal-content
+ = form_tag import_csv_namespace_project_issues_path, multipart: true do
+ .modal-header
+ %h3
+ = _('Import issues')
+ .svg-content.import-export-svg-container
+ = image_tag 'illustrations/export-import.svg', alt: _('Import/Export illustration'), class: 'illustration'
+ %a.close{ href: '#', 'data-dismiss' => 'modal' } ×
+ .modal-body
+ .modal-text
+ %p
+ = _("Your issues will be imported in the background. Once finished, you'll get a confirmation email.")
+ .form-group
+ = label_tag :file, _('Upload CSV file'), class: 'label-bold'
+ %div
+ = file_field_tag :file, accept: '.csv,text/csv', required: true
+ %p.text-secondary
+ = _('It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.')
+ = _('The maximum file size allowed is %{size}.') % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
+ .modal-footer
+ %button{ type: 'submit', class: 'btn btn-success', title: _('Import issues'), data: { track_label: "export_issues_csv", track_event: "click_button"} }
+ = _('Import issues')
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 1e7737aeb97..39e9e9171cf 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,8 +11,7 @@
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls
- = render "projects/issues/nav_btns"
+ = render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
@@ -23,4 +22,4 @@
- if new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
- = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
+ = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project), show_import_button: true
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 8c2fe2625c7..3a674da6e87 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -14,8 +14,14 @@
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
= sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
- %span.d-none.d-sm-block
- Closed
+ .d-none.d-sm-block
+ - if @issue.moved?
+ - moved_link_start = "<a href=\"#{issue_path(@issue.moved_to)}\" class=\"text-white text-underline\">".html_safe
+ - moved_link_end = '</a>'.html_safe
+ = s_('IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})').html_safe % {moved_link_start: moved_link_start,
+ moved_link_end: moved_link_end}
+ - else
+ = _("Closed")
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
= sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block Open
@@ -55,7 +61,7 @@
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
- = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
@@ -85,7 +91,6 @@
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' unless @issue.confidential?
- %section.issuable-discussion
- = render 'projects/issues/discussion'
+ = render_if_exists 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 59592abcf6a..afea5268006 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -8,10 +8,6 @@
.nav-controls
- if can?(current_user, :update_build, @project)
- - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord
- = link_to 'Cancel running', cancel_all_project_jobs_path(@project),
- data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 56b06374d6d..bb7c297ba1f 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -5,15 +5,10 @@
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
-- if labels_or_filters && can_admin_label
- - content_for(:header_content) do
- .nav-controls
- = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
-
- if labels_or_filters
#promote-label-modal
%div{ class: container_class }
- = render 'shared/labels/nav'
+ = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
.labels-container.prepend-top-10
- if can_admin_label
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 13b967beba1..a7c9e54506d 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,4 +1,3 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
- html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
- data: { markdown_version: @merge_request.cached_markdown_version } do |f|
+ html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, presenter: @mr_presenter
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index faa070d0389..b8e0b66e277 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -34,7 +34,7 @@
= merge_request.target_branch
- if merge_request.labels.any?
&nbsp;
- - merge_request.labels.each do |label|
+ - labels_sorted_by_title(merge_request.labels).each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
.issuable-meta
@@ -46,9 +46,9 @@
%li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
CLOSED
- - if merge_request.head_pipeline
+ - if can?(current_user, :read_pipeline, merge_request.head_pipeline)
%li.issuable-pipeline-status.d-none.d-sm-inline-block
- = render_pipeline_status(merge_request.head_pipeline)
+ = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user)
- if merge_request.open? && merge_request.broken?
%li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
@@ -56,6 +56,7 @@
- if merge_request.assignee
%li
= link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name'))
+ = render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request
= render 'shared/issuable_meta_data', issuable: merge_request
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index bd6f1c05949..57fbd360d46 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,5 +1,5 @@
%ul.content-list.mr-list.issuable-list
- - if @merge_requests.exists?
+ - if @merge_requests.present?
= render @merge_requests
- else
= render 'shared/empty_states/merge_requests'
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d6f340d0ee2..5111c9fab8d 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -7,7 +7,7 @@
- page_card_attributes @merge_request.card_attributes
- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
-.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
+.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -59,6 +59,7 @@
#js-vue-discussion-counter
.tab-content#diff-notes-app
+ #js-diff-file-finder
#notes.notes.tab-pane.voting_notes
.row
%section.col-md-12
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index ebd3229e42b..5cc6b5a173b 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,27 +1,28 @@
= form_for [@project.namespace.becomes(Namespace), @project, @milestone],
- html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'},
- data: { markdown_version: @milestone.cached_markdown_version } do |f|
+ html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
= form_errors(@milestone)
.row
.col-md-6
.form-group.row
- = f.label :title, "Title", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :title, _('Title')
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "qa-milestone-title form-control", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: 'qa-milestone-title form-control', required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, "Description", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :description, _('Description')
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: 'Write milestone description...'
+ = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: _('Write milestone description...')
= render 'shared/notes/hints'
.clearfix
.error-alert
- = render "shared/milestones/form_dates", f: f
+ = render 'shared/milestones/form_dates', f: f
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-success btn qa-milestone-create-button"
- = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel"
+ = f.submit _('Create milestone'), class: 'btn-create btn qa-milestone-create-button'
+ = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel'
- else
- = f.submit 'Save changes', class: "btn-success btn"
- = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel"
+ = f.submit _('Save changes'), class: 'btn-success btn'
+ = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'btn btn-cancel'
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 4006a468792..aa564e00af9 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,14 +1,14 @@
- @no_container = true
-- breadcrumb_title "Edit"
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
-- page_title "Edit", @milestone.title, "Milestones"
+- breadcrumb_title _('Edit')
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
+- page_title _('Edit'), @milestone.title, _('Milestones')
%div{ class: container_class }
%h3.page-title
- Edit Milestone
+ = _('Edit Milestone')
%hr
- = render "form"
+ = render 'form'
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 57f3c640696..a3414c16d73 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,15 +1,16 @@
- @no_container = true
-- page_title 'Milestones'
+- page_title _('Milestones')
%div{ class: container_class }
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
+ = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do
- New milestone
+ = link_to new_project_milestone_path(@project), class: 'btn btn-success qa-new-project-milestone', title: _('New milestone') do
+ = _('New milestone')
.milestones
#delete-milestone-modal
@@ -20,6 +21,6 @@
- if @milestones.blank?
%li
- .nothing-here-block No milestones to show
+ .nothing-here-block= _('No milestones to show')
= paginate @milestones, theme: 'gitlab'
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 01cc951e8c2..79207fd70b5 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,12 +1,12 @@
- @no_container = true
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
-- breadcrumb_title "New"
-- page_title "New Milestone"
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
+- breadcrumb_title _('New')
+- page_title _('New Milestone')
%div{ class: container_class }
%h3.page-title
- New Milestone
+ = _('New Milestone')
%hr
- = render "form"
+ = render 'form'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5859de61d71..0542b349e44 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,30 +1,30 @@
- @no_container = true
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
- breadcrumb_title @milestone.title
-- page_title @milestone.title, "Milestones"
+- page_title @milestone.title, _('Milestones')
- page_description @milestone.description
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
- Closed
+ = _('Closed')
- elsif @milestone.expired?
- Past due
+ = _('Past due')
- elsif @milestone.upcoming?
- Upcoming
+ = _('Upcoming')
- else
- Open
+ = _('Open')
.header-text-content
%span.identifier
%strong
- Milestone
+ = _('Milestone')
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
- = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
+ = link_to edit_project_milestone_path(@project, @milestone), class: 'btn btn-grouped btn-nr' do
+ = _('Edit')
- if @project.group
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
@@ -39,13 +39,13 @@
#promote-milestone-modal
- if @milestone.active?
- = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+ = link_to _('Close milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: 'btn btn-close btn-nr btn-grouped'
- else
- = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
+ = link_to _('Reopen milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: 'btn btn-reopen btn-nr btn-grouped'
= render 'shared/milestones/delete_button'
- %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: '#' }
= icon('angle-double-left')
.detail-page-description.milestone-detail
@@ -62,10 +62,10 @@
- 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.
+ %span= _('Assign some issues to this milestone.')
- elsif @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close this milestone now.
+ %span= _('All issues for this milestone are closed. You may close this milestone now.')
= render 'deprecation_message'
= render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 21b105e6f80..c031815200b 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -57,7 +57,7 @@
%td
- if mirror.last_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
- %td.mirror-action-buttons
+ %td
.btn-group.mirror-actions-group.pull-right{ role: 'group' }
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'))
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index a760d02c4c3..ff7c36c2d5b 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,6 +16,9 @@
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ %p
+ - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank'
+ = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
.md
= brand_new_project_guidelines
%p
@@ -33,7 +36,7 @@
%span.d-block.d-sm-none Blank
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' }
- %span.d-none.d-sm-block Create from template
+ %span.d-none.d-sm-block.qa-project-create-from-template-tab Create from template
%span.d-block.d-sm-none Template
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index ae8c801b705..138e2864bad 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -9,4 +9,4 @@
.form-actions
= link_to 'Remove pages', project_pages_path(@project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
- else
- .nothing-here-block Only the project owner can remove pages
+ .nothing-here-block Only project maintainers can remove pages
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
index ce3ef29c32e..74478ee011c 100644
--- a/app/views/projects/pages/_https_only.html.haml
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -3,7 +3,7 @@
.form-check
= f.check_box :pages_https_only, class: 'form-check-input', disabled: pages_https_only_disabled?
= f.label :pages_https_only, class: pages_https_only_label_class do
- %strong Force domains with SSL certificates to use HTTPS
+ %strong Force HTTPS (requires valid certificates)
- unless pages_https_only_disabled?
.prepend-top-10
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index 0d848f7899c..b7b46c56c37 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -6,25 +6,24 @@
.form-group.row
= f.label :domain, class: 'col-form-label col-sm-2' do
- Domain
+ = _("Domain")
.col-sm-10
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
.form-group.row
= f.label :certificate, class: 'col-form-label col-sm-2' do
- Certificate (PEM)
+ = _("Certificate (PEM)")
.col-sm-10
= f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline Upload a certificate for your domain with all intermediates
+ %span.help-inline= _("Upload a certificate for your domain with all intermediates")
.form-group.row
= f.label :key, class: 'col-form-label col-sm-2' do
- Key (PEM)
+ = _("Key (PEM)")
.col-sm-10
= f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline Upload a private key for your certificate
+ %span.help-inline= _("Upload a private key for your certificate")
- else
.nothing-here-block
- Support for custom certificates is disabled.
- Ask your system's administrator to enable it.
+ = _("Support for custom certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index 342b1482df7..e11387ae742 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -1,4 +1,4 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
- page_title @domain.domain
%h3.page-title
@@ -8,4 +8,4 @@
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Save Changes', class: "btn btn-success"
+ = f.submit _('Save Changes'), class: "btn btn-success"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 94ad1470052..c7cefa87c76 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,12 +1,12 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
-- page_title 'New Pages Domain'
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
+- page_title _('New Pages Domain')
%h3.page-title
- New Pages Domain
+ = _("New Pages Domain")
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Create New Domain', class: "btn btn-success"
+ = f.submit _('Create New Domain'), class: "btn btn-success"
.float-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index a8484187493..82147568981 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,6 +1,6 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
-- page_title "#{@domain.domain}", 'Pages Domains'
+- page_title "#{@domain.domain}", _('Pages Domains')
- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}."
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
@@ -9,37 +9,37 @@
= content_for :flash_message do
.alert.alert-warning
.container-fluid.container-limited
- This domain is not verified. You will need to verify ownership before access is enabled.
+ = _("This domain is not verified. You will need to verify ownership before access is enabled.")
%h3.page-title.with-button
- = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
- Pages Domain
+ = link_to _('Edit'), edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
+ = _("Pages Domain")
.table-holder
%table.table
%tr
%td
- Domain
+ = _("Domain")
%td
= link_to @domain.url do
= @domain.url
= icon('external-link')
%tr
%td
- DNS
+ = _("DNS")
%td
.input-group
= text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
%p.form-text.text-muted
- To access this domain create a new DNS record
+ = _("To access this domain create a new DNS record")
- if verification_enabled
- verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
%tr
%td
- Verification status
+ = _("Verification status")
%td
= form_tag verify_project_pages_domain_path(@project, @domain) do
.status-badge
@@ -53,17 +53,16 @@
.input-group-append
= clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
- To #{link_to 'verify ownership', help_link} of your domain,
- add the above key to a TXT record within to your DNS configuration.
+ - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record'))
+ = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help }
%tr
%td
- Certificate
+ = _("Certificate")
%td
- if @domain.certificate_text
%pre
= @domain.certificate_text
- else
.light
- missing
+ = _("missing")
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 259979417e0..1121cf06b5c 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -34,7 +34,7 @@
= n_('Reveal value', 'Reveal values', @schedule.variables.size)
.form-group.row
.col-md-9
- = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
+ = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold'
%div
= f.check_box :active, required: false, value: @schedule.active?
= _('Active')
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 0f0114d513c..55adeb345ab 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -6,25 +6,24 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
- - if commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.total_size, "job"
- - if @pipeline.ref
- from
- - if @pipeline.ref_exists?
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - else
- %span.ref-name
- = @pipeline.ref
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ - if @pipeline.ref_exists?
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - else
+ %span.ref-name
+ = @pipeline.ref
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
- .well-segment
+ .well-segment.qa-pipeline-badges
.icon-container
= sprite_icon('flag')
- if @pipeline.latest?
@@ -49,7 +48,7 @@
content: "<a class='autodevops-link' href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>",
} }
Auto DevOps
- - if @pipeline.merge_request?
+ - if @pipeline.merge_request_event?
%span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "This pipeline is run in a merge request context" }
merge request
- if @pipeline.stuck?
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index c23fe6ff170..c0ac79ed5f8 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -1,7 +1,7 @@
-%div
- %p.light
- = _("Commit duration in minutes for last 30 commits")
+%p.light
+ = _("Commit duration in minutes for last 30 commits")
+%div
%canvas#build_timesChart{ height: 200 }
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 14b3d47a9c2..47f1f074210 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -13,18 +13,21 @@
%p.light
= _("Pipelines for last week")
(#{date_from_to(Date.today - 7.days, Date.today)})
- %canvas#weekChart{ height: 200 }
+ %div
+ %canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last month")
(#{date_from_to(Date.today - 30.days, Date.today)})
- %canvas#monthChart{ height: 200 }
+ %div
+ %canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last year")
- %canvas#yearChart.padded{ height: 250 }
+ %div
+ %canvas#yearChart.padded{ height: 250 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 3f05e06b0c6..b5d397e3065 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,7 +1,6 @@
.card.project-members-groups
.card-header
- Groups with access to
- %strong= @project.name
+ = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(@project.name) }
%span.badge.badge-pill= group_links.size
%ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml
index 88e68f89024..079811e4e79 100644
--- a/app/views/projects/project_members/_new_project_group.html.haml
+++ b/app/views/projects/project_members/_new_project_group.html.haml
@@ -10,8 +10,9 @@
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- = link_to _("Read more"), help_page_path("user/permissions")
- about role permissions
+ - permissions_docs_path = help_page_path('user/permissions')
+ - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path }
+ = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.form-group
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
.clearable-input
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 1de7d9c6957..0590578c3fe 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -2,20 +2,21 @@
.col-sm-12
= form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f|
.form-group
- = label_tag :user_ids, "Select members to invite", class: "label-bold"
+ = label_tag :user_ids, _("Select members to invite"), class: "label-bold"
= users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
.form-group
- = label_tag :access_level, "Choose a role permission", class: "label-bold"
+ = label_tag :access_level, _("Choose a role permission"), class: "label-bold"
.select-wrapper
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions")
- about role permissions
+ - permissions_docs_path = help_page_path('user/permissions')
+ - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path }
+ = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.form-group
.clearable-input
- = label_tag :expires_at, 'Access expiration date', class: 'label-bold'
+ = label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- = f.submit "Add to project", class: "btn btn-success qa-add-member-button"
- = link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project"
+ = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button"
+ = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 9682f8ac922..e0dd386fc5d 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -4,14 +4,13 @@
.card
.card-header.flex-project-members-panel
%span.flex-project-title
- Members of
- %strong= project.name
+ = _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(project.name) }
%span.badge.badge-pill= members.total_count
- = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
.position-relative
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search")
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list.qa-members-list
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 8b93e81cd31..bcca943de6a 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -1,15 +1,15 @@
-- page_title "Import members"
+- page_title _("Import members")
%h3.page-title
- Import members from another project
+ = _("Import members from another project")
%p.light
- Only project members will be imported. Group members will be skipped.
+ = _("Only project members will be imported. Group members will be skipped.")
%hr
= form_tag apply_import_project_project_members_path(@project), method: 'post' do
.form-group.row
- = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2'
+ = label_tag :source_project_id, _("Project"), class: 'col-form-label col-sm-2'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
- = button_tag 'Import project members', class: "btn btn-success"
- = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel"
+ = button_tag _('Import project members'), class: "btn btn-success"
+ = link_to _("Cancel"), project_project_members_path(@project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 14ed3345765..242ff91f539 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,39 +1,34 @@
-- page_title "Members"
+- page_title _("Members")
.row.prepend-top-default
.col-lg-12
%h4
- Project members
+ = _("Project members")
- if can?(current_user, :admin_project_member, @project)
%p
- You can invite a new member to
- %strong= @project.name
- or invite another group.
+ = _("You can invite a new member to <strong>%{project_name}</strong> or invite another group.").html_safe % { project_name: sanitize_project_name(@project.name) }
- else
%p
- Members can be added by project
- %i Maintainers
- or
- %i Owners
+ = _("Members can be added by project <i>Maintainers</i> or <i>Owners</i>").html_safe
.light
- if can?(current_user, :admin_project_member, @project)
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
- if @project.allowed_to_share_with_group?
%li.nav-tab{ role: 'presentation' }
- %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite group")
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render 'projects/project_members/new_project_member', tab_title: 'Invite member'
+ = render 'projects/project_members/new_project_member', tab_title: _('Invite member')
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
- = render 'projects/project_members/new_project_group', tab_title: 'Invite group'
+ = render 'projects/project_members/new_project_group', tab_title: _('Invite group')
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix
%h5.member.existing-title
- Existing members and groups
+ = _("Existing members and groups")
- if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links
diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml
index 5b4d8927045..6159f1c3542 100644
--- a/app/views/projects/project_templates/_built_in_templates.html.haml
+++ b/app/views/projects/project_templates/_built_in_templates.html.haml
@@ -1,7 +1,7 @@
- Gitlab::ProjectTemplate.all.each do |template|
.template-option.d-flex.align-items-center
- .logo.append-right-10
- = custom_icon(template.logo, size: 40)
+ .logo.append-right-10.px-1
+ = image_tag template.logo, size: 32, class: "btn-template-icon icon-#{template.name}"
.description
%strong
= template.title
@@ -12,6 +12,6 @@
%a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview")
%label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name }
- %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
+ %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 539b184e5c2..4997770321e 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -12,7 +12,7 @@
%p
By default, protected branches are designed to:
%ul
- %li prevent their creation, if not already created, from everybody except Maintainers
+ %li prevent their creation, if not already created, from everybody except users who are allowed to merge
%li prevent pushes from everybody except Maintainers
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 81b07af22ad..bb7998f739d 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -15,7 +15,7 @@
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha')
= time_ago_with_tooltip(commit.committed_date)
- else
- (branch was removed from repository)
+ (branch was deleted from repository)
= yield
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index f01d4e826b9..28bb4e032eb 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -2,4 +2,4 @@
- page_title _('Releases')
%div{ class: container_class }
- #js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases') } }
+ #js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases/index') } }
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index a6c16c70313..a24ada53bac 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -32,6 +32,6 @@
- else
%h4.underlined-title
- = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count }
+ = _('Available group Runners: %{runners}').html_safe % { runners: @group_runners.count }
%ul.bordered-list
= render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
index f650fa0f38f..635580eac5c 100644
--- a/app/views/projects/serverless/functions/index.html.haml
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -5,7 +5,7 @@
- status_path = project_serverless_functions_path(@project, format: :json)
- clusters_path = project_clusters_path(@project)
-.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
+.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
.js-serverless-functions-notice
diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml
new file mode 100644
index 00000000000..29737b7014a
--- /dev/null
+++ b/app/views/projects/serverless/functions/show.html.haml
@@ -0,0 +1,16 @@
+- @no_container = true
+- @content_class = "limit-container-width" unless fluid_layout
+
+- add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project))
+
+- page_title @service[:name]
+
+.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json } }
+%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
+ .top-area.adjust
+ .serverless-function-details#js-serverless-function-details
+
+ .js-serverless-function-notice
+ .flash-container
+
+ .function-holder.js-function-holder.input-group
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml
index 597c029f755..a1d74b91002 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_metrics.html.haml
@@ -1,6 +1,6 @@
- project = local_assigns.fetch(:project)
-.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
+.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } }
.card-header
= s_('PrometheusService|Common metrics')
%span.badge.badge-pill.js-monitored-count 0
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 1d0b0265bb7..6aafa85e99a 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -4,7 +4,9 @@
= s_('PrometheusService|Metrics')
%p
= s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/metrics'), target: '_blank', rel: "noopener noreferrer"
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
= render 'projects/services/prometheus/metrics', project: @project
+
+= render_if_exists 'projects/services/prometheus/external_alerts', project: @project
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 5ec5a06396e..8c4d1c32ebe 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -4,10 +4,6 @@
= form_errors(@project)
%fieldset.builds-feature.js-auto-devops-settings
.form-group
- - message = auto_devops_warning_message(@project)
- - if message
- %p.auto-devops-warning-message.settings-message.text-center
- = message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.card.auto-devops-card
.card-body
@@ -21,19 +17,12 @@
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' }
- = form.label :domain do
- %strong= _('Domain')
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .form-text.text-muted
- = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
- - if cluster_ingress_ip = cluster_ingress_ip(@project)
- = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
-
+ %p.settings-message.text-center
+ - kubernetes_cluster_link = help_page_path('user/project/clusters/index')
+ - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
+ = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe }
%label.prepend-top-10
%strong= s_('CICD|Deployment strategy')
- %p.settings-message.text-center
- = s_('CICD|Deployment strategy needs a domain name to work correctly.')
.form-check
= form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index bb328f5344c..bfb275b9ef5 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -110,6 +110,9 @@
%li
go test -cover (Go)
%code coverage: \d+.\d+% of statements
+ %li
+ nyc npm test (NodeJS) -
+ %code All files[^|]*\|[^|]*\s+([\d\.]+)
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
new file mode 100644
index 00000000000..6b15331db01
--- /dev/null
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -0,0 +1,18 @@
+- return unless can?(current_user, :read_environment, @project)
+
+- setting = error_tracking_setting
+
+%section.settings.expanded.border-0.no-animate
+ .settings-header
+ %h4
+ = _('Error Tracking')
+ %p
+ = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.')
+ = link_to _('More information'), help_page_path('user/project/operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ .js-error-tracking-form{ data: { list_projects_endpoint: list_projects_project_error_tracking_index_path(@project, format: :json),
+ operations_settings_endpoint: project_settings_operations_path(@project),
+ project: error_tracking_setting_project_json,
+ api_host: setting.api_host,
+ enabled: setting.enabled.to_json,
+ token: setting.token } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
new file mode 100644
index 00000000000..2822debe426
--- /dev/null
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -0,0 +1,6 @@
+- @content_class = 'limit-container-width' unless fluid_layout
+- page_title _('Operations Settings')
+- breadcrumb_title _('Operations Settings')
+
+= render 'projects/settings/operations/error_tracking', expanded: true
+= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index a4974d89c1a..7682d01a5a1 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,12 +1,16 @@
- page_title _("Snippets")
-- if current_user
- .top-area
- - include_private = @project.team.member?(current_user) || current_user.admin?
- = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
+- if @snippets.exists?
+ - if current_user
+ .top-area
+ - include_private = @project.team.member?(current_user) || current_user.admin?
+ = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
- .nav-controls
- if can?(current_user, :create_project_snippet, @project)
- = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-success", title: _("New snippet")
+ .nav-controls
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-success", title: _("New snippet")
-= render 'snippets/snippets'
+ = render 'shared/snippets/list'
+- else
+ = render 'shared/empty_states/snippets', button_path: new_namespace_project_snippet_path(@project.namespace, @project)
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 26b333d4ecf..d64e3a49a81 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title _("New")
-- page_title _("New Snippets")
+- page_title _("New Snippet")
%h3.page-title
- = _('New Snippet')
+ = _("New Snippet")
%hr
= render "shared/snippets/form", url: project_snippets_path(@project, @snippet)
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index f55202c2c5f..cc203cfad86 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -28,7 +28,7 @@
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
+ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 026bc44a05f..458096f9dd6 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -27,7 +27,7 @@
- if can?(current_user, :push_code, @project)
= link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn' do
= s_('TagsPage|New tag')
- = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn rss-btn has-tooltip' do
+ = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn d-none d-sm-inline-block has-tooltip' do
= icon("rss")
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/tags/releases/edit.html.haml b/app/views/projects/tags/releases/edit.html.haml
index 52c6c7ec424..e4efeed04f0 100644
--- a/app/views/projects/tags/releases/edit.html.haml
+++ b/app/views/projects/tags/releases/edit.html.haml
@@ -12,8 +12,7 @@
= form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
- html: { class: 'common-note-form release-form js-quick-submit' },
- data: { markdown_version: @release.cached_markdown_version }) do |f|
+ html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 15a960f81b8..8ee1407d9d9 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -20,10 +20,10 @@
.nav-controls.controls-flex
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
+ = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
= link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do
- = icon('files-o')
+ = sprite_icon('folder-open')
= link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
= icon('history')
.btn-container.controls-item
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 4e9a119ac66..ec8e5234bd4 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -40,27 +40,24 @@
#{ _('New directory') }
- elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
%li
- - continue_params = { to: project_new_blob_path(@project, @id),
- notice: edit_in_new_fork_notice,
+ - continue_params = { to: project_new_blob_path(@project, @id),
+ notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
= link_to fork_path, method: :post do
#{ _('New file') }
%li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
= link_to fork_path, method: :post do
#{ _('Upload file') }
%li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
= link_to fork_path, method: :post do
#{ _('New directory') }
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 7e4618e1a88..6f6f1e5e0c5 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- - if can?(current_user, :admin_trigger, trigger)
+ - if trigger.has_token_exposed?
%span= trigger.token
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
- else
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 7d8826e540c..5bb69563b51 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,13 +1,9 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
-- if params[:legacy_render] || !commonmark_for_repositories_enabled?
- - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION
-- else
- - markdown_version = 0
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
- data: { markdown_version: markdown_version, uploads_path: uploads_path } do |f|
+ data: { uploads_path: uploads_path } do |f|
= form_errors(@page)
- if @page.persisted?
@@ -16,7 +12,7 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control', value: @page.title
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- if @page.persisted?
%span.edit-wiki-page-slug-tip
= icon('lightbulb-o')
@@ -31,7 +27,7 @@
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
= render 'shared/notes/hints'
.clearfix
@@ -47,14 +43,14 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
- = f.submit _("Save changes"), class: 'btn-success btn'
+ = f.submit _("Save changes"), class: 'btn-success btn qa-save-changes-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
- = f.submit s_("Wiki|Create page"), class: 'btn-success btn'
+ = f.submit s_("Wiki|Create page"), class: 'btn-success btn qa-create-page-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 02c5a6ea55c..28353927135 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -12,7 +12,7 @@
.blocks-container
.block.block-first
- if @sidebar_page
- = render_wiki_content(@sidebar_page, legacy_render_context(params))
+ = render_wiki_content(@sidebar_page)
- else
%ul.wiki-pages
= render @sidebar_wiki_entries, context: 'sidebar'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 26671a7b7d2..1277ea6c743 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -23,9 +23,6 @@
= s_("Wiki|Create Page")
.nav-controls
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
- = s_("Wiki|New page")
- if @page.persisted?
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aeef64fd7eb..94267b6e0cf 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
+- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 4d5fd55364c..8e1c054b50c 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.human_title, _("Wiki")
-- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
+- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -27,6 +27,6 @@
.prepend-top-default.append-bottom-default
.wiki.md{ class: ('use-csslab' if Feature.enabled?(:csslab)) }
- = render_wiki_content(@page, legacy_render_context(params))
+ = render_wiki_content(@page)
= render 'sidebar'
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index ff9a7b53a86..aaf9b973cda 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -6,75 +6,75 @@
- if project_search_tabs?(:blobs)
%li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
- Code
+ = _("Code")
%span.badge.badge-pill
= @search_results.blobs_count
- if project_search_tabs?(:issues)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
- Issues
+ = _("Issues")
%span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
+ = _("Merge requests")
%span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
- Milestones
+ = _("Milestones")
%span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
- Comments
+ = _("Comments")
%span.badge.badge-pill
= limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
- Wiki
+ = _("Wiki")
%span.badge.badge-pill
= @search_results.wiki_blobs_count
- if project_search_tabs?(:commits)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
- Commits
+ = _("Commits")
%span.badge.badge-pill
= @search_results.commits_count
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
- Snippet Contents
+ = _("Snippet Contents")
%span.badge.badge-pill
= @search_results.snippet_blobs_count
%li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
- Titles and Filenames
+ = _("Titles and Filenames")
%span.badge.badge-pill
= @search_results.snippet_titles_count
- else
%li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
- Projects
+ = _("Projects")
%span.badge.badge-pill
= limited_count(@search_results.limited_projects_count)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
- Issues
+ = _("Issues")
%span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
+ = _("Merge requests")
%span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
- Milestones
+ = _("Milestones")
%span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index 6837f64f132..c8b6a3258ab 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -3,31 +3,31 @@
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown
- %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:", group_id: params[:group_id] } }
+ %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Group:'), group_id: params[:group_id] } }
%span.dropdown-toggle-text
- Group:
+ = _("Group:")
- if @group.present?
= @group.name
- else
- Any
+ = _("Any")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
- = dropdown_title("Filter results by group")
- = dropdown_filter("Search groups")
+ = dropdown_title(_("Filter results by group"))
+ = dropdown_filter(_("Search groups"))
= dropdown_content
= dropdown_loading
.dropdown.project-filter
- %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
+ %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Project:') } }
%span.dropdown-toggle-text
- Project:
+ = _("Project:")
- if @project.present?
= @project.full_name
- else
- Any
+ = _("Any")
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
- = dropdown_title("Filter results by project")
- = dropdown_filter("Search projects")
+ = dropdown_title(_("Filter results by project"))
+ = dropdown_filter(_("Search projects"))
= dropdown_content
= dropdown_loading
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index a4a5cec1314..4af0c6bf84a 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -4,12 +4,12 @@
.search-holder
.search-field-holder
- = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
+ = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
= icon("search", class: "search-icon")
%button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" }
= icon("times-circle")
%span.sr-only
- Clear search
+ = _("Clear search")
- unless params[:snippets].eql? 'true'
= render 'filter'
- = button_tag "Search", class: "btn btn-success btn-search"
+ = button_tag _("Search"), class: "btn btn-success btn-search"
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index ab56f48ba4d..be7a2436d16 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,4 +1,4 @@
-- if @search_objects.empty?
+- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
- else
.row-content-block
@@ -6,9 +6,11 @@
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
- in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]}
+ - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project])
+ = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
- elsif @group
- in group #{link_to @group.name, @group}
+ - link_to_group = link_to(@group.name, @group)
+ = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
.results.prepend-top-10
- if @scope == 'commits'
diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml
index 821a39d61f5..9d15995bb51 100644
--- a/app/views/search/results/_empty.html.haml
+++ b/app/views/search/results/_empty.html.haml
@@ -2,5 +2,5 @@
.search_glyph
%h4
= icon('search')
- We couldn't find any results matching
+ = _("We couldn't find any results matching")
%code= @search_term
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index c413c3d4abb..796782035f2 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -4,7 +4,7 @@
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
- if issue.closed?
- %span.badge.badge-danger.prepend-left-5 Closed
+ %span.badge.badge-danger.prepend-left-5= _("Closed")
.float-right ##{issue.iid}
- if issue.description.present?
.description.term
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 519176af108..f0e0af11f27 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -3,9 +3,9 @@
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
- %span.badge.badge-primary.prepend-left-5 Merged
+ %span.badge.badge-primary.prepend-left-5= _("Merged")
- elsif merge_request.closed?
- %span.badge.badge-danger.prepend-left-5 Closed
+ %span.badge.badge-danger.prepend-left-5= _("Closed")
.float-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index e4ab7b0541f..6717939d034 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -6,14 +6,14 @@
%h5.note-search-caption.str-truncated
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
- commented on
- = link_to project.full_name, project
+ - link_to_project = link_to(project.full_name, project)
+ = _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project }
&middot;
- if note.for_commit?
- = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do
+ = link_to_if(noteable_identifier, _("Commit %{commit_id}") % { commit_id: truncate_sha(note.commit_id) }, note_url) do
= truncate_sha(note.commit_id)
- %span.light Commit deleted
+ %span.light= _("Commit deleted")
- else
%span #{note.noteable_type.titleize} ##{noteable_identifier}
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index b4ecd7bbae9..a60a4501557 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -21,10 +21,10 @@
.file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = markup(snippet.file_name, chunk[:data], legacy_render_context(params))
+ = markup(snippet.file_name, chunk[:data])
- else
.file-content.code
- .nothing-here-block Empty file
+ .nothing-here-block= _("Empty file")
- else
.file-content.code.js-syntax-highlight
.line-numbers
@@ -42,4 +42,4 @@
= highlight(snippet.file_name, chunk[:data])
- else
.file-content.code
- .nothing-here-block Empty file
+ .nothing-here-block= _("Empty file")
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 9c8afb2165b..1e01088d9e6 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -5,7 +5,7 @@
- if snippet_title.private?
%span.badge.badge-gray
%i.fa.fa-lock
- private
+ = _("private")
%span.cgray.monospace.tiny.float-right.term
= snippet_title.file_name
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 499697f2777..3260d05f509 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,5 +1,5 @@
- @hide_top_links = true
-- breadcrumb_title "Search"
+- breadcrumb_title _("Search")
- page_title @search_term
.prepend-top-default
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index 7d3e243495f..ca392e1adfc 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,17 +1,16 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
-- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
+- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
- Unsubscribe from #{noteable_type}
+ = _("Unsubscribe from %{type}") % { type: noteable_type }
%p
- = succeed '?' do
- Are you sure you want to unsubscribe from the #{noteable_type}:
- = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
+ - link_to_noteable_text = link_to(noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]))
+ = _("Are you sure you want to unsubscribe from the %{type}: %{link_to_noteable_text}?").html_safe % { type: noteable_type, link_to_noteable_text: link_to_noteable_text }
%p
- = link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true),
+ = link_to _('Unsubscribe'), unsubscribe_sent_notification_path(@sent_notification, force: true),
class: 'btn btn-primary append-right-10'
- = link_to 'Cancel', new_user_session_path, class: 'btn append-right-10'
+ = link_to _('Cancel'), new_user_session_path, class: 'btn append-right-10'
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index 0d0a3c1aa64..755fd3a17d3 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,5 +1,5 @@
-- if show_auto_devops_implicitly_enabled_banner?(project)
- .auto-devops-implicitly-enabled-banner.alert.alert-warning
+- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
+ .qa-auto-devops-banner.auto-devops-implicitly-enabled-banner.alert.alert-warning
- more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link'
- auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link }
= auto_devops_message.html_safe
diff --git a/app/views/shared/_flash_user_callout.html.haml b/app/views/shared/_flash_user_callout.html.haml
new file mode 100644
index 00000000000..fe175195e66
--- /dev/null
+++ b/app/views/shared/_flash_user_callout.html.haml
@@ -0,0 +1,11 @@
+- callout_data = { uid: "callout_feature_#{feature_name}_dismissed", feature_id: feature_name, dismiss_endpoint: user_callouts_path }
+- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
+
+.flash-container.flash-container-page.user-callout{ data: callout_data }
+ -# We currently only support `alert`, `warning`, `notice`, `success`
+ %div{ class: "flash-#{flash_type}" }
+ %div{ class: "#{(container_class unless fluid_layout)} #{(extra_flash_class unless @no_container)} #{@content_class}" }
+ %span= message
+ %button.btn.btn-default.close.js-close{ type: 'button',
+ 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', css_class: 'dismiss-icon')
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 6cc8c485666..31a5370a5f8 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -1,4 +1,4 @@
-- note_count = @issuable_meta_data[issuable.id].notes_count
+- note_count = @issuable_meta_data[issuable.id].user_notes_count
- 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')
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 28b34e38b15..8607f87ce0b 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -7,7 +7,6 @@
.stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= sprite_icon(icon_status)
- = icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index 6e2527bd1a1..1e6b6f7c79b 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -13,3 +13,4 @@
- if http_enabled?
%li
= dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
+ = render_if_exists 'shared/mobile_kerberos_clone'
diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml
index 3150d39b84a..a8d3de66418 100644
--- a/app/views/shared/_personal_access_tokens_created_container.html.haml
+++ b/app/views/shared/_personal_access_tokens_created_container.html.haml
@@ -6,7 +6,7 @@
= container_title
.form-group
.input-group
- = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block"
+ = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block"
%span.input-group-append
= clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard")
%span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again.
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index f4df7bdcd83..0891b3459ec 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -12,7 +12,7 @@
.row
.form-group.col-md-6
= f.label :name, class: 'label-bold'
- = f.text_field :name, class: "form-control", required: true
+ = f.text_field :name, class: "form-control qa-personal-access-token-name-field", required: true
.row
.form-group.col-md-6
@@ -26,4 +26,4 @@
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
.prepend-top-default
- = f.submit "Create #{type} token", class: "btn btn-success"
+ = f.submit "Create #{type} token", class: "btn btn-success qa-create-token-button"
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
index 2efd03d4867..49f3aae0f98 100644
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -29,7 +29,7 @@
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- 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 float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+ %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right qa-revoke-button", 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.
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index c6c5cadc3f5..307a0919a4c 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,6 +1,6 @@
.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
- .board-inner
+ .board-inner.d-flex.flex-column
%header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
%h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index 1ff956649ed..c9ff63f8c45 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
+%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index 913c065e188..eb7808573b9 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -15,7 +15,7 @@
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to 'here', help_page_path('ssh/README')
- = form.text_area :key, class: 'form-control thin_area', rows: 5
+ = form.text_area :key, class: 'form-control thin-area', rows: 5
- else
= form.label :fingerprint, class: 'col-form-label col-sm-2'
.col-sm-10
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 0ddc56dc6c3..9173b802dd4 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -1,6 +1,11 @@
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
+- show_import_button = local_assigns.fetch(:show_import_button, false) && can?(current_user, :import_issues, @project)
- has_button = button_path || project_select_button
+- closed_issues_count = issuables_count_for_state(:issues, :closed)
+- opened_issues_count = issuables_count_for_state(:issues, :opened)
+- is_opened_state = params[:state] == 'opened'
+- is_closed_state = params[:state] == 'closed'
.row.empty-state
.col-12
@@ -13,6 +18,20 @@
= _("Sorry, your filter produced no results")
%p.text-center
= _("To widen your search, change or remove filters above")
+ - if show_new_issue_link?(@project)
+ .text-center
+ = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", title: _("New issue"), id: "new_issue_body_link"
+ - elsif is_opened_state && opened_issues_count == 0 && closed_issues_count > 0
+ %h4.text-center
+ = _("There are no open issues")
+ %p.text-center
+ = _("To keep this project going, create a new issue")
+ - if show_new_issue_link?(@project)
+ .text-center
+ = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", title: _("New issue"), id: "new_issue_body_link"
+ - elsif is_closed_state && opened_issues_count > 0 && closed_issues_count == 0
+ %h4.text-center
+ = _("There are no closed issues")
- elsif current_user
%h4
= _("The Issue Tracker is the place to add things that need to be improved or solved in a project")
@@ -21,12 +40,20 @@
- if has_button
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues'
+ = render 'shared/new_project_item_select', path: 'issues/new', label: _('New issue'), type: :issues, with_feature_enabled: 'issues'
- else
- = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
+ = link_to _('New issue'), button_path, class: 'btn btn-success', title: _('New issue'), id: 'new_issue_link'
+
+ - if show_import_button
+ = render 'projects/issues/import_csv/button', type: :text
+
- else
%h4.text-center= _("There are no issues to show")
%p
= _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.")
.text-center
= link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success'
+
+- if show_import_button
+ = render 'projects/issues/import_csv/modal'
+
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index bee26cd8312..a739103641e 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,6 +1,6 @@
.row.empty-state.labels
.col-12
- .svg-content
+ .svg-content.qa-label-svg
= image_tag 'illustrations/labels.svg'
.col-12
.text-content
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 06ceb9738bc..be5b1c6b6ce 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -1,6 +1,11 @@
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
+- closed_merged_count = issuables_count_for_state(:merged, :closed)
+- opened_merged_count = issuables_count_for_state(:merged, :opened)
+- is_opened_state = params[:state] == 'opened'
+- is_closed_state = params[:state] == 'closed'
+- can_create_merge_request = merge_request_source_project_for_project(@project)
.row.empty-state.merge-requests
.col-12
@@ -13,6 +18,20 @@
= _("Sorry, your filter produced no results")
%p.text-center
= _("To widen your search, change or remove filters above")
+ .text-center
+ - if can_create_merge_request
+ = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link"
+ - elsif is_opened_state && opened_merged_count == 0 && closed_merged_count > 0
+ %h4.text-center
+ = _("There are no open merge requests")
+ %p.text-center
+ = _("To keep this project going, create a new merge request")
+ .text-center
+ - if can_create_merge_request
+ = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link"
+ - elsif is_closed_state && opened_merged_count > 0 && closed_merged_count == 0
+ %h4.text-center
+ = _("There are no closed merge requests")
- else
%h4
= _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others")
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
index 555cb4f4af9..bba3475d244 100644
--- a/app/views/shared/empty_states/_priority_labels.html.haml
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -1,4 +1,4 @@
.text-center
- .svg-content
+ .svg-content.qa-label-svg
= image_tag 'illustrations/priority_labels.svg'
%p Star labels to start sorting by priority
diff --git a/app/views/shared/empty_states/_profile_tabs.html.haml b/app/views/shared/empty_states/_profile_tabs.html.haml
new file mode 100644
index 00000000000..6da40e1b059
--- /dev/null
+++ b/app/views/shared/empty_states/_profile_tabs.html.haml
@@ -0,0 +1,19 @@
+- current_user_empty_message_description = local_assigns.fetch(:current_user_empty_message_description, nil)
+- secondary_button_link = local_assigns.fetch(:secondary_button_link, nil)
+
+.nothing-here-block
+ .svg-content
+ = image_tag illustration_path, size: '75'
+ .text-content
+ - if user_profile? and current_user.present? and current_user.username == params[:username]
+ %h5= current_user_empty_message_header
+
+ - if current_user_empty_message_description.present?
+ %p= current_user_empty_message_description
+
+ - if secondary_button_link.present?
+ = link_to secondary_button_label, secondary_button_link, class: 'btn btn-create btn-inverted'
+
+ = link_to primary_button_label, primary_button_link, class: 'btn btn-success'
+ - else
+ %h5= visitor_empty_message
diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml
new file mode 100644
index 00000000000..a1a16b9d067
--- /dev/null
+++ b/app/views/shared/empty_states/_snippets.html.haml
@@ -0,0 +1,20 @@
+- button_path = local_assigns.fetch(:button_path, false)
+
+.row.empty-state
+ .col-12
+ .svg-content
+ = image_tag 'illustrations/snippets_empty.svg'
+ .text-content
+ - if current_user
+ %h4
+ = s_('SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep.')
+ %p
+ = s_('SnippetsEmptyState|They can be either public or private.')
+ .text-center
+ = link_to s_('SnippetsEmptyState|New snippet'), button_path, class: 'btn btn-success', title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link'
+ - unless current_page?(dashboard_snippets_path)
+ = link_to s_('SnippetsEmptyState|Explore public snippets'), explore_snippets_path, class: 'btn btn-default', title: s_('SnippetsEmptyState|Explore public snippets')
+ - else
+ %h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.')
+
+
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index df3308abe0d..73eedcc1dc9 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -2,7 +2,7 @@
- if can?(current_user, :create_wiki, @project)
- create_path = project_wiki_path(@project, params[:id], { view: 'create' })
- - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page')
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
index a5f100e3469..d44017299b8 100644
--- a/app/views/shared/empty_states/_wikis_layout.html.haml
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -1,6 +1,6 @@
.row.empty-state
.col-12
- .svg-content
+ .svg-content.qa-svg-content
= image_tag image_path
.col-12
.text-content.text-center
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a1b901aaffa..609b8dce21a 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -14,7 +14,7 @@
%span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
- .avatar-container.s40
+ .avatar-container.rect-avatar.s40
= link_to group do
= group_icon(group, class: "avatar s40 d-none d-sm-block")
.title
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
index f50a6bd4d6a..c5b39c7db08 100644
--- a/app/views/shared/groups/_list.html.haml
+++ b/app/views/shared/groups/_list.html.haml
@@ -1,3 +1,10 @@
+- illustration_path = 'illustrations/profile-page/groups.svg'
+- current_user_empty_message_header = s_('UserProfile|You can create a group for several dependent projects.')
+- current_user_empty_message_description = s_('UserProfile|Groups are the best way to manage projects and members.')
+- primary_button_label = _('New group')
+- primary_button_link = new_group_path
+- visitor_empty_message = s_('GroupsEmptyState|No groups found')
+
- if groups.any?
- user = local_assigns[:user]
@@ -5,4 +12,9 @@
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group, user: user
- else
- .nothing-here-block= s_("GroupsEmptyState|No groups found")
+ = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: illustration_path,
+ current_user_empty_message_header: current_user_empty_message_header,
+ current_user_empty_message_description: current_user_empty_message_description,
+ primary_button_label: primary_button_label,
+ primary_button_link: primary_button_link,
+ visitor_empty_message: visitor_empty_message }
diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg
deleted file mode 100644
index a51e81e5568..00000000000
--- a/app/views/shared/icons/_express.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg>
diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg
deleted file mode 100644
index 852bd183cc7..00000000000
--- a/app/views/shared/icons/_rails.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg>
diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg
deleted file mode 100644
index ccf18749029..00000000000
--- a/app/views/shared/icons/_spring.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg>
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 4597d9439fa..fd413bd68c8 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -1,7 +1,7 @@
.dropdown.prepend-left-10#js-add-list
%button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
+ .dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= 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, board.parent)
= render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index ca02424215c..909eb738f95 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -2,7 +2,7 @@
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden
- = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.float-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
@@ -50,4 +50,3 @@
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]
-
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index d4834090413..83f60fa6fe2 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
= icon('rss')
-= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do
+= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
= custom_icon('icon_calendar')
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
deleted file mode 100644
index 2ca4657851c..00000000000
--- a/app/views/shared/issuable/_filter.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.issues-filters
- .issues-details-filters.row-content-block.second-block
- = 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]
- .issues-other-filters
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
-
- .filter-item.inline
- - if params[:assignee_id].present?
- = hidden_field_tag(:assignee_id, params[:assignee_id])
- = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.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, show_started: true
-
- .filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
-
- - unless @no_filters_set
- .float-right
- = render 'shared/issuable/sort_dropdown'
-
- - has_labels = @labels && @labels.any?
- .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- - if has_labels
- = render 'shared/labels_row', labels: @labels
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 46634693067..f43be304e6b 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,7 +11,7 @@
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
- .check-all-holder.hidden
+ .check-all-holder.d-none.d-sm-block.hidden
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
@@ -57,10 +57,10 @@
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
@@ -71,18 +71,19 @@
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
+ = render_if_exists 'shared/issuable/approver_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
- %li.filter-dropdown-item{ data: { value: 'upcoming' } }
+ %li.filter-dropdown-item{ data: { value: 'Upcoming' } }
%button.btn.btn-link{ type: 'button' }
= _('Upcoming')
- %li.filter-dropdown-item{ data: { value: 'started' } }
+ %li.filter-dropdown-item{ data: { value: 'Started' } }
%button.btn.btn-link{ type: 'button' }
= _('Started')
%li.divider.droplab-item-ignore
@@ -92,10 +93,10 @@
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
@@ -107,10 +108,10 @@
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
@@ -128,6 +129,14 @@
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
+ #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Yes')
+ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('No')
= render_if_exists 'shared/issuable/filter_weight', type: type
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0520eda37a4..9596c1df20e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -133,7 +133,7 @@
#js-confidential-entry-point
-# haml-lint:disable InlineJavaScript
- %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
+ %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
.js-sidebar-participants-entry-point
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index c5cce1823f0..1a59055f652 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -25,7 +25,7 @@
.value.hide-collapsed
- if issuable_sidebar[:assignee]
= link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- - if issuable_sidebar[:assignee][:can_merge]
+ - unless issuable_sidebar[:assignee][:can_merge]
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index b6ea9185b10..967f31c8325 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -5,7 +5,7 @@
.dropdown.inline.prepend-left-10.issue-sort-dropdown
.btn-group{ role: 'group' }
.btn-group{ role: 'group' }
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 1881875b7c0..f0c4acdd07f 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -10,7 +10,7 @@
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input'
= label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do
- Remove source branch when merge request is accepted.
+ Delete source branch when merge request is accepted.
.form-group.row
.col-sm-10.offset-sm-2
@@ -19,4 +19,4 @@
= check_box_tag 'merge_request[squash]', '1', issuable.squash, class: 'form-check-input'
= label_tag 'merge_request[squash]', class: 'form-check-label' do
Squash commits when merge request is accepted.
- = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge')
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
index 98572db738b..e69246dd0eb 100644
--- a/app/views/shared/labels/_nav.html.haml
+++ b/app/views/shared/labels/_nav.html.haml
@@ -18,3 +18,7 @@
%button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') }
= icon("search")
= render 'shared/labels/sort_dropdown'
+ - if labels_or_filters && can_admin_label && @project
+ = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new"
+ - if labels_or_filters && can_admin_label && @group
+ = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success qa-label-create-new"
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
deleted file mode 100644
index ebae58f28ba..00000000000
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- model_name = source.model_name.to_s.downcase
-
-- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord
- .project-action-button.inline
- - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
- = link_to link_text, polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: leave_confirmation_message(source) },
- class: 'btn'
-- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
- .project-action-button.inline
- = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(requester) },
- class: 'btn'
-- elsif source.request_access_enabled && can?(current_user, :request_access, source)
- .project-action-button.inline
- = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 6b3841ebbc4..2db1f67a793 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -10,7 +10,7 @@
- if user
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info
- = link_to user.name, user_path(user), class: 'member'
+ = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
= user_status(user)
%span.cgray= user.to_reference
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 922805958a5..4de89d7c7a0 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,11 +1,13 @@
.col-md-6
.form-group.row
- = f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :start_date, "Start Date"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.form-group.row
- = f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :due_date, "Due Date"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index ed7fefba56d..e75f0a184ea 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,5 +1,5 @@
- dashboard = local_assigns[:dashboard]
-- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
+- custom_dom_id = dom_id(milestone.try(:milestone) ? milestone.milestone : milestone)
- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
@@ -21,16 +21,18 @@
= milestone.group.full_name
- if milestone.legacy_group_milestone?
.projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
- = dashboard ? milestone.project.full_name : milestone.project.name
+ - link_to milestone_path(milestone.milestone) do
+ %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
+ = dashboard ? milestone.project.full_name : milestone.project.name
+ - if milestone.project
+ .label-badge.label-badge-gray.d-inline-block
+ = milestone.project.full_name
.col-sm-4.milestone-progress
= milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
- = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
+ = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path
.float-lg-right.light #{milestone.percent_complete(current_user)}% complete
.col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
@@ -58,5 +60,5 @@
- else
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
- if dashboard
- .status-box.status-box-milestone
+ .label-badge.label-badge-gray
= milestone_type
diff --git a/app/views/shared/milestones/_search_form.html.haml b/app/views/shared/milestones/_search_form.html.haml
new file mode 100644
index 00000000000..403a0224a85
--- /dev/null
+++ b/app/views/shared/milestones/_search_form.html.haml
@@ -0,0 +1,8 @@
+= form_tag request.path, method: :get do |f|
+ = search_field_tag :search_title, params[:search_title],
+ placeholder: _('Filter by milestone name'),
+ class: 'form-control input-short',
+ spellcheck: false
+ = hidden_field_tag :state, params[:state]
+ = hidden_field_tag :sort, params[:sort]
+
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 55460acab8f..b877f66c71e 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -12,7 +12,7 @@
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
- %span.badge.badge-pill= milestone.merge_requests.size
+ %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- else
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
@@ -21,11 +21,11 @@
%li.nav-item
= link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants
- %span.badge.badge-pill= milestone.participants.count
+ %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
%li.nav-item
= link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels
- %span.badge.badge-pill= milestone.labels.count
+ %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 0499b04a482..55b1c14022f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -62,20 +62,19 @@
%th Open issues
%th State
%th Due date
- - milestone.milestones.each do |ms|
%tr
%td
- - project_name = group ? ms.project.name : ms.project.full_name
- = link_to project_name, project_milestone_path(ms.project, ms)
+ - project_name = group ? milestone.project.name : milestone.project.full_name
+ = link_to project_name, milestone_path(milestone.milestone)
%td
- = ms.issues_visible_to_user(current_user).opened.count
+ = milestone.milestone.issues_visible_to_user(current_user).opened.count
%td
- - if ms.closed?
+ - if milestone.closed?
Closed
- else
Open
%td
- = ms.expires_at
+ = milestone.expires_at
- elsif milestone.group_milestone?
%br
View
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 0674c822d63..c3f5eeb0da6 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,30 +1,31 @@
- noteable_name = @note.noteable.human_class_name
.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
- %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
+ %input.btn.btn-nr.btn-success.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment') }
- if @note.can_be_discussion_note?
- = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
+ = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle btn-success js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
= icon('caret-down', class: 'toggle-icon')
%ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
- %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
+ %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong Comment
+ %strong= _("Comment")
%p
- Add a general comment to this #{noteable_name}.
+ = _("Add a general comment to this %{noteable_name}.") % { noteable_name: noteable_name }
%li.divider.droplab-item-ignore
- %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
+ %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start discussion'), 'close-text' => _("Start discussion & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start discussion & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong Start discussion
+ %strong= _("Start discussion")
%p
= succeed '.' do
- Discuss a specific suggestion or question
- if @note.noteable.supports_resolvable_notes?
- that needs to be resolved
+ = _('Discuss a specific suggestion or question that needs to be resolved')
+ - else
+ = _('Discuss a specific suggestion or question')
diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml
index f4b3aac29b4..84a3ef9d8fe 100644
--- a/app/views/shared/notes/_edit.html.haml
+++ b/app/views/shared/notes/_edit.html.haml
@@ -1 +1 @@
-%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note
+%textarea.hidden.js-task-list-field.original-task-list{ data: { update_url: note_url(note) } }= note.note
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index fec966069b9..6fe511c2999 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -3,12 +3,12 @@
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
- = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…"
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: _("Write a comment or drag your files here…")
= render 'shared/notes/hints'
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning
- Finish editing this message first!
- = submit_tag 'Save comment', class: 'btn btn-nr btn-success js-comment-save-button'
+ = _("Finish editing this message first!")
+ = submit_tag _('Save comment'), class: 'btn btn-nr btn-success js-comment-save-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
- Cancel
+ = _("Cancel")
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index c360f1ffe2a..6a1eea85fde 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -29,7 +29,7 @@
= render 'projects/zen', f: f,
attr: :note,
classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here…",
+ placeholder: _("Write a comment or drag your files here…"),
supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
@@ -40,5 +40,5 @@
= yield(:note_actions)
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
- Discard draft
+ %a.btn.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: { cancel_text: _("Cancel") } }
+ = _('Cancel')
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 00eae553279..46f3f8428f1 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -1,10 +1,10 @@
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
.comment-toolbar.clearfix
.toolbar-text
- = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+ = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_quick_actions
and
- = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
+ = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
are
- else
is
@@ -24,12 +24,12 @@
= icon('file-image-o', class: 'toolbar-button-icon')
%span.uploading-error-message
-# Populated by app/assets/javascripts/dropzone_input.js
- %button.retry-uploading-link{ type: 'button' } Try again
+ %button.retry-uploading-link{ type: 'button' }= _("Try again")
or
- %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file
+ %button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
%button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+ = _("Attach a file")
- %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel
+ %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel")
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index e125d7f108a..41d6ae79c81 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -54,7 +54,7 @@
.note-text.md
= markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
- .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore, markdown_version: note.cached_markdown_version } }
+ .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
- if note_editable
= render 'shared/notes/edit', note: note
@@ -62,7 +62,7 @@
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
.system-note-commit-list-toggler.hide
- Toggle commit list
+ = _("Toggle commit list")
%i.fa.fa-angle-down
- if note.attachment.url
.note-attachment
@@ -74,5 +74,5 @@
= icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_project_note_path(note.project, note),
- title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
+ title: _('Delete this attachment'), method: :delete, remote: true, data: { confirm: _('Are you sure you want to remove the attachment?') }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 4c4050c6054..002189e6ecd 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -19,20 +19,14 @@
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
.disabled-comment.text-center.prepend-top-default
- Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link'
- or
- = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
- to comment
+ - link_to_register = link_to(_("register"), new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link')
+ - link_to_sign_in = link_to(_("sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link')
+ = _("Please %{link_to_register} or %{link_to_sign_in} to comment").html_safe % { link_to_register: link_to_register, link_to_sign_in: link_to_sign_in }
- elsif discussion_locked
.disabled-comment.text-center.prepend-top-default
%span.issuable-note-warning
= sprite_icon('lock', size: 16, css_class: 'icon')
%span
- This
- = issuable.class.to_s.titleize.downcase
- is locked. Only
- %b project members
- can comment.
+ = _("This %{issuable} is locked. Only <strong>project members</strong> can comment.").html_safe % { issuable: issuable.class.to_s.titleize.downcase }
-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 30860988bbb..2ece7b7f701 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,7 +1,7 @@
- btn_class = local_assigns.fetch(:btn_class, nil)
- if notification_setting
- .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
+ .js-notification-dropdown.notification-dropdown.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
new file mode 100644
index 00000000000..6d26dbebbc8
--- /dev/null
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -0,0 +1,27 @@
+- btn_class = local_assigns.fetch(:btn_class, nil)
+
+- if notification_setting
+ .js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ = hidden_field_tag "hide_label", true
+ = f.hidden_field :level, class: "notification_setting_level"
+ .js-notification-toggle-btns
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
+ = sprite_icon("arrow-down", css_class: "icon mr-0")
+ .sr-only Toggle dropdown
+ - else
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ %span.js-notification-loading.fa.hidden
+ = sprite_icon("arrow-down", css_class: "icon")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
+
+ = content_for :scripts_body do
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 15c29e14cc0..13847cd9be1 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -14,6 +14,17 @@
- skip_pagination = false unless local_assigns[:skip_pagination] == true
- compact_mode = false unless local_assigns[:compact_mode] == true
- css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
+- contributed_projects_illustration_path = 'illustrations/profile-page/contributed-projects.svg'
+- contributed_projects_current_user_empty_message_header = s_('UserProfile|Explore public groups to find projects to contribute to.')
+- contributed_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t contributed to any projects')
+- own_projects_illustration_path = 'illustrations/profile-page/personal-project.svg'
+- own_projects_current_user_empty_message_header = s_('UserProfile|You haven\'t created any personal projects.')
+- own_projects_current_user_empty_message_description = s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.')
+- own_projects_visitor_empty_message = s_('UserProfile|This user doesn\'t have any personal projects')
+- primary_button_label = _('New project')
+- primary_button_link = new_project_path
+- secondary_button_label = _('Explore groups')
+- secondary_button_link = explore_groups_path
.js-projects-list-holder
- if any_projects?(projects)
@@ -33,4 +44,18 @@
%span &nbsp;you have no access to.
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- .nothing-here-block No projects found
+ - if @contributed_projects
+ = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: contributed_projects_illustration_path,
+ current_user_empty_message_header: contributed_projects_current_user_empty_message_header,
+ primary_button_label: primary_button_label,
+ primary_button_link: primary_button_link,
+ secondary_button_label: secondary_button_label,
+ secondary_button_link: secondary_button_link,
+ visitor_empty_message: contributed_projects_visitor_empty_message }
+ - else
+ = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: own_projects_illustration_path,
+ current_user_empty_message_header: own_projects_current_user_empty_message_header,
+ current_user_empty_message_description: own_projects_current_user_empty_message_description,
+ primary_button_label: primary_button_label,
+ primary_button_link: primary_button_link,
+ visitor_empty_message: own_projects_visitor_empty_message }
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index fea7e17be3d..d2b1be29eb9 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,21 +12,21 @@
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
-- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block"
-- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row"
+- css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between"
+- avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar'
%li.project-row.d-flex{ class: css_class }
= cache(cache_key) do
- if avatar
- .avatar-container.s64.flex-grow-0.flex-shrink-0
+ .avatar-container.s48.flex-grow-0.flex-shrink-0{ class: avatar_container_class }
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
- = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:''
+ = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s48", alt:''
- else
- = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64)
- .project-details.flex-sm-fill{ class: css_details_class }
- .flex-wrapper.flex-fill
- .d-flex.align-items-center.flex-wrap
+ = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48)
+ .project-details.d-sm-flex.flex-sm-fill.align-items-center
+ .flex-wrapper
+ .d-flex.align-items-center.flex-wrap.project-title
%h2.d-flex.prepend-top-8
= link_to project_path(project), class: 'text-plain' do
%span.project-full-name.append-right-8><
@@ -52,13 +52,13 @@
%span.user-access-role.d-block= Gitlab::Access.human_access(access)
- if show_last_commit_as_description
- .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ .description.d-none.d-sm-block.append-right-default
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present?
- .description.d-none.d-sm-block.prepend-top-8.append-right-default
+ .description.d-none.d-sm-block.append-right-default
= markdown_field(project, :description)
- .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
+ .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class }
.icon-container.d-flex.align-items-center
- if project.archived
%span.d-flex.icon-wrapper.badge.badge-warning archived
@@ -74,34 +74,19 @@
= number_with_delimiter(project.forks_count)
- if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project),
- class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip",
+ class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip",
title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_merge_requests_count)
- if show_issue_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project),
- class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip",
+ class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip",
title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count)
- - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
+ - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
+ - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
- = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
+ = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top', path: pipeline_path
.updated-note
%span Updated #{updated_tooltip}
-
- .d-none.d-lg-flex.align-item-stretch
- - unless compact_mode
- - if current_user
- %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } }
- - if current_user.starred?(project)
- = sprite_icon('star', { css_class: 'icon' })
- %span.starred= s_('ProjectOverview|Unstar')
- - else
- = sprite_icon('star-o', { css_class: 'icon' })
- %span= s_('ProjectOverview|Star')
-
- - else
- = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
- = sprite_icon('star-o', { css_class: 'icon' })
- %span= s_('ProjectOverview|Star')
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index cf9c3055499..3007da0c189 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -3,8 +3,7 @@
.snippet-form-holder
= form_for @snippet, url: url,
- html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
- data: { markdown_version: @snippet.cached_markdown_version } do |f|
+ html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
= form_errors(@snippet)
.form-group.row
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 10bfc30492a..a43296aa806 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -30,7 +30,7 @@
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- - if public_snippet?
+ - if @snippet.embeddable?
.embed-snippet
.input-group
.input-group-prepend
diff --git a/app/views/shared/snippets/_list.html.haml b/app/views/shared/snippets/_list.html.haml
new file mode 100644
index 00000000000..5d2152eb411
--- /dev/null
+++ b/app/views/shared/snippets/_list.html.haml
@@ -0,0 +1,12 @@
+- remote = local_assigns.fetch(:remote, false)
+- link_project = local_assigns.fetch(:link_project, false)
+
+- if @snippets.exists?
+ .snippets-list-holder
+ %ul.content-list
+ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
+
+ = paginate @snippets, theme: 'gitlab', remote: remote
+
+- else
+ .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 5069e2e4ca6..42af97bc6af 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -25,7 +25,7 @@
#{snippet.to_reference} &middot;
authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
by
- = link_to user_snippets_path(snippet.author) do
+ = link_to user_snippets_path(snippet.author), class: "js-user-link", data: { user_id: snippet.author.id } do
= snippet.author_name
- if link_project && snippet.project_id?
%span.d-none.d-sm-inline-block
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index af9db5f59a8..a5d3e1c8de0 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -4,6 +4,6 @@
- scopes.each do |scope|
%fieldset.form-group.form-check
- = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input'
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio"
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label'
.text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 69d41f8fe5e..dab247da251 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,10 +1,21 @@
- link_project = local_assigns.fetch(:link_project, false)
+- illustration_path = 'illustrations/profile-page/activity.svg'
+- current_user_empty_message_header = s_('UserProfile|You haven\'t created any snippets.')
+- current_user_empty_message_description = s_('UserProfile|Snippets in GitLab can either be private, internal, or public.')
+- primary_button_label = _('New snippet')
+- primary_button_link = new_snippet_path
+- visitor_empty_message = s_('UserProfile|No snippets found.')
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
- .nothing-here-block= _("Nothing here.")
+ = render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: illustration_path,
+ current_user_empty_message_header: current_user_empty_message_header,
+ current_user_empty_message_description: current_user_empty_message_description,
+ primary_button_label: primary_button_label,
+ primary_button_link: primary_button_link,
+ visitor_empty_message: visitor_empty_message }
= paginate @snippets, theme: 'gitlab'
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 4f418e2381f..f5fe75bed5a 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -10,4 +10,4 @@
= link_to user_path(@user) do
= _("%{user_name} profile page") % { user_name: @user.name }
-= render 'snippets'
+= render 'shared/snippets/list'
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index 55799e10a46..6d7a52c7688 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -1,5 +1,5 @@
.clearfix
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
- .avatar-container.s40
+ .avatar-container.rect-avatar.s40
= group_icon(group, class: 'avatar group-avatar s40')
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index d22905ecc93..b5bc1180290 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -9,24 +9,22 @@
.col-md-12.col-lg-6
- if can?(current_user, :read_cross_project)
.activities-block
- .append-right-5
- .prepend-top-16
- .d-flex.align-items-center.border-bottom
- %h4.flex-grow
- = s_('UserProfile|Activity')
- = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_path } }
- .center.light.loading
- = spinner nil, true
-
- .col-md-12.col-lg-6
- .projects-block
- .prepend-left-5
.prepend-top-16
.d-flex.align-items-center.border-bottom
%h4.flex-grow
- = s_('UserProfile|Personal projects')
- = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_projects_path } }
+ = s_('UserProfile|Activity')
+ = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
+ .overview-content-list{ data: { href: user_path } }
.center.light.loading
= spinner nil, true
+
+ .col-md-12.col-lg-6
+ .projects-block
+ .prepend-top-16
+ .d-flex.align-items-center.border-bottom
+ %h4.flex-grow
+ = s_('UserProfile|Personal projects')
+ = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
+ .overview-content-list{ data: { href: user_projects_path } }
+ .center.light.loading
+ = spinner nil, true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dd2cd36eac2..211e3eafac6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -71,14 +71,14 @@
= icon('twitter-square')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'noopener noreferrer nofollow'
+ = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider
- = icon('map-marker')
+ = sprite_icon('location', size: 16, css_class: 'vertical-align-sub')
= @user.location
- unless @user.organization.blank?
.profile-link-holder.middle-dot-divider
- = icon('briefcase')
+ = sprite_icon('work', size: 16, css_class: 'vertical-align-sub')
= @user.organization
- if @user.bio.present?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d3cf21db335..d86f654dd44 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -23,6 +23,7 @@
- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
+- gcp_cluster:cluster_upgrade_app
- gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
@@ -45,6 +46,11 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- hashed_storage:hashed_storage_migrator
+- hashed_storage:hashed_storage_rollbacker
+- hashed_storage:hashed_storage_project_migrate
+- hashed_storage:hashed_storage_project_rollback
+
- mail_scheduler:mail_scheduler_issue_due
- mail_scheduler:mail_scheduler_notification_service
@@ -90,13 +96,16 @@
- object_pool:object_pool_join
- object_pool:object_pool_destroy
+- container_repository:delete_container_repository
+- container_repository:cleanup_container_repository
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
- authorized_projects
- background_migration
+- chat_notification
- create_gpg_signature
-- delete_container_repository
- delete_merged_branches
- delete_user
- email_receiver
@@ -120,7 +129,6 @@
- project_cache
- project_destroy
- project_export
-- project_migrate_hashed_storage
- project_service
- propagate_service_template
- reactive_caching
@@ -129,7 +137,6 @@
- repository_fork
- repository_import
- repository_remove_remote
-- storage_migrator
- system_hook_push
- update_merge_requests
- upload_checksum
@@ -140,3 +147,5 @@
- detect_repository_languages
- repository_cleanup
- delete_stored_files
+- import_issues_csv
+- project_daily_statistics
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 61d866b1f02..adc38226405 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -9,14 +9,27 @@ class BuildFinishedWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- # We execute that in sync as this access the files in order to access local file, and reduce IO
- BuildTraceSectionsWorker.new.perform(build.id)
- BuildCoverageWorker.new.perform(build.id)
-
- # We execute that async as this are two independent operations that can be executed after TraceSections and Coverage
- BuildHooksWorker.perform_async(build.id)
- ArchiveTraceWorker.perform_async(build.id)
+ process_build(build)
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ # Processes a single CI build that has finished.
+ #
+ # This logic resides in a separate method so that EE can extend it more
+ # easily.
+ #
+ # @param [Ci::Build] build The build to process.
+ def process_build(build)
+ # We execute these in sync to reduce IO.
+ BuildTraceSectionsWorker.new.perform(build.id)
+ BuildCoverageWorker.new.perform(build.id)
+
+ # We execute these async as these are independent operations.
+ BuildHooksWorker.perform_async(build.id)
+ ArchiveTraceWorker.perform_async(build.id)
+ ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
+ end
end
diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb
new file mode 100644
index 00000000000..25a306e94d8
--- /dev/null
+++ b/app/workers/chat_notification_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class ChatNotificationWorker
+ include ApplicationWorker
+
+ RESCHEDULE_INTERVAL = 2.seconds
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ send_response(build)
+ end
+ rescue Gitlab::Chat::Output::MissingBuildSectionError
+ # The creation of traces and sections appears to be eventually consistent.
+ # As a result it's possible for us to run the above code before the trace
+ # sections are present. To better handle such cases we'll just reschedule
+ # the job instead of producing an error.
+ self.class.perform_in(RESCHEDULE_INTERVAL, build_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def send_response(build)
+ Gitlab::Chat::Responder.responder_for(build).try do |responder|
+ if build.success?
+ output = Gitlab::Chat::Output.new(build)
+
+ responder.success(output.to_s)
+ else
+ responder.failure
+ end
+ end
+ end
+end
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
new file mode 100644
index 00000000000..974ee8c8146
--- /dev/null
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class CleanupContainerRepositoryWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ queue_namespace :container_repository
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :container_repository, :current_user
+
+ def perform(current_user_id, container_repository_id, params)
+ @current_user = User.find_by_id(current_user_id)
+ @container_repository = ContainerRepository.find_by_id(container_repository_id)
+
+ return unless valid?
+
+ try_obtain_lease do
+ Projects::ContainerRepository::CleanupTagsService
+ .new(project, current_user, params)
+ .execute(container_repository)
+ end
+ end
+
+ private
+
+ def valid?
+ current_user && container_repository && project
+ end
+
+ def project
+ container_repository&.project
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_key
+ @lease_key ||= "container_repository:cleanup_tags:#{container_repository.id}"
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_release?
+ # we don't allow to execute this worker
+ # more often than LEASE_TIMEOUT
+ # for given container repository
+ false
+ end
+end
diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb
new file mode 100644
index 00000000000..d1a538859b4
--- /dev/null
+++ b/app/workers/cluster_upgrade_app_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ClusterUpgradeAppWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::UpgradeService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index e8fe9d82797..42e66513ff1 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -4,6 +4,8 @@ class DeleteContainerRepositoryWorker
include ApplicationWorker
include ExclusiveLeaseGuard
+ queue_namespace :container_repository
+
LEASE_TIMEOUT = 1.hour
attr_reader :container_repository
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index dce812d1ae2..251e95c68d5 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -4,8 +4,20 @@ class ExpireBuildArtifactsWorker
include ApplicationWorker
include CronjobQueue
- # rubocop: disable CodeReuse/ActiveRecord
def perform
+ if Feature.enabled?(:ci_new_expire_job_artifacts_service, default_enabled: true)
+ perform_efficient_artifacts_removal
+ else
+ perform_legacy_artifacts_removal
+ end
+ end
+
+ def perform_efficient_artifacts_removal
+ Ci::DestroyExpiredJobArtifactsService.new.execute
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform_legacy_artifacts_removal
Rails.logger.info 'Scheduling removal of build artifacts'
build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index c96e8a0379b..2c070d482a1 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -11,16 +11,9 @@ class ExpirePipelineCacheWorker
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
- project = pipeline.project
store = Gitlab::EtagCaching::Store.new
- store.touch(project_pipelines_path(project))
- store.touch(project_pipeline_path(project, pipeline))
- store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
- store.touch(new_merge_request_pipelines_path(project))
- each_pipelines_merge_request_path(project, pipeline) do |path|
- store.touch(path)
- end
+ update_etag_cache(pipeline, store)
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
end
@@ -44,11 +37,30 @@ class ExpirePipelineCacheWorker
Gitlab::Routing.url_helpers.project_new_merge_request_path(project, format: :json)
end
- def each_pipelines_merge_request_path(project, pipeline)
+ def each_pipelines_merge_request_path(pipeline)
pipeline.all_merge_requests.each do |merge_request|
- path = Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(project, merge_request, format: :json)
+ path = Gitlab::Routing.url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json)
yield(path)
end
end
+
+ # Updates ETag caches of a pipeline.
+ #
+ # This logic resides in a separate method so that EE can more easily extend
+ # it.
+ #
+ # @param [Ci::Pipeline] pipeline
+ # @param [Gitlab::EtagCaching::Store] store
+ def update_etag_cache(pipeline, store)
+ project = pipeline.project
+
+ store.touch(project_pipelines_path(project))
+ store.touch(project_pipeline_path(project, pipeline))
+ store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
+ store.touch(new_merge_request_pipelines_path(project))
+ each_pipelines_merge_request_path(pipeline) do |path|
+ store.touch(path)
+ end
+ end
end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index d3628b23189..b33e9b1f718 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -23,6 +23,7 @@ class GitGarbageCollectWorker
end
task = task.to_sym
+ project.link_pool_repository
gitaly_call(task, project.repository.raw_repository)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
diff --git a/app/workers/hashed_storage/base_worker.rb b/app/workers/hashed_storage/base_worker.rb
new file mode 100644
index 00000000000..816e0504db6
--- /dev/null
+++ b/app/workers/hashed_storage/base_worker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class BaseWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 30.seconds.to_i
+ LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'.freeze
+
+ protected
+
+ def lease_key
+ # we share the same lease key for both migration and rollback so they don't run simultaneously
+ "#{LEASE_KEY_SEGMENT}:#{project_id}"
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+ end
+end
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
new file mode 100644
index 00000000000..49e347d4060
--- /dev/null
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class MigratorWorker
+ include ApplicationWorker
+
+ queue_namespace :hashed_storage
+
+ # @param [Integer] start initial ID of the batch
+ # @param [Integer] finish last ID of the batch
+ def perform(start, finish)
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_migrate(start: start, finish: finish)
+ end
+ end
+end
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
new file mode 100644
index 00000000000..f00a459a097
--- /dev/null
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class ProjectMigrateWorker < BaseWorker
+ include ApplicationWorker
+
+ queue_namespace :hashed_storage
+
+ attr_reader :project_id
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(project_id, old_disk_path = nil)
+ @project_id = project_id # we need to set this in order to create the lease_key
+
+ try_obtain_lease do
+ project = Project.without_deleted.find_by(id: project_id)
+ break unless project
+
+ old_disk_path ||= project.disk_path
+
+ ::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
new file mode 100644
index 00000000000..55e1d7ab23e
--- /dev/null
+++ b/app/workers/hashed_storage/project_rollback_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class ProjectRollbackWorker < BaseWorker
+ include ApplicationWorker
+
+ queue_namespace :hashed_storage
+
+ attr_reader :project_id
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(project_id, old_disk_path = nil)
+ @project_id = project_id # we need to set this in order to create the lease_key
+
+ try_obtain_lease do
+ project = Project.without_deleted.find_by(id: project_id)
+ break unless project
+
+ old_disk_path ||= project.disk_path
+
+ ::Projects::HashedStorage::RollbackService.new(project, old_disk_path, logger: logger).execute
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
new file mode 100644
index 00000000000..a4da8443787
--- /dev/null
+++ b/app/workers/hashed_storage/rollbacker_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class RollbackerWorker
+ include ApplicationWorker
+
+ queue_namespace :hashed_storage
+
+ # @param [Integer] start initial ID of the batch
+ # @param [Integer] finish last ID of the batch
+ def perform(start, finish)
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_rollback(start: start, finish: finish)
+ end
+ end
+end
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
new file mode 100644
index 00000000000..d44fdfec8ae
--- /dev/null
+++ b/app/workers/import_issues_csv_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class ImportIssuesCsvWorker
+ include ApplicationWorker
+
+ sidekiq_retries_exhausted do |job|
+ Upload.find(job['args'][2]).destroy
+ end
+
+ def perform(current_user_id, project_id, upload_id)
+ @user = User.find(current_user_id)
+ @project = Project.find(project_id)
+ @upload = Upload.find(upload_id)
+
+ importer = Issues::ImportCsvService.new(@user, @project, @upload.build_uploader)
+ importer.execute
+
+ @upload.destroy
+ end
+end
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 4726e416182..421fbf04e28 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -8,14 +8,64 @@ module MailScheduler
include MailSchedulerQueue
def perform(meth, *args)
- deserialized_args = ActiveJob::Arguments.deserialize(args)
+ check_arguments!(args)
+ deserialized_args = ActiveJob::Arguments.deserialize(args)
notification_service.public_send(meth, *deserialized_args) # rubocop:disable GitlabSecurity/PublicSend
rescue ActiveJob::DeserializationError
+ # No-op.
+ # This exception gets raised when an argument
+ # is correct (deserializeable), but it still cannot be deserialized.
+ # This can happen when an object has been deleted after
+ # rails passes this job to sidekiq, but before
+ # sidekiq gets it for execution.
+ # In this case just do nothing.
end
def self.perform_async(*args)
- super(*ActiveJob::Arguments.serialize(args))
+ super(*Arguments.serialize(args))
+ end
+
+ private
+
+ # If an argument is in the ActiveJob::Arguments::TYPE_WHITELIST list,
+ # it means the argument cannot be deserialized.
+ # Which means there's something wrong with our code.
+ def check_arguments!(args)
+ args.each do |arg|
+ if arg.class.in?(ActiveJob::Arguments::TYPE_WHITELIST)
+ raise(ArgumentError, "Argument `#{arg}` cannot be deserialized because of its type")
+ end
+ end
+ end
+
+ # Permit ActionController::Parameters for serializable Hash
+ #
+ # Port of
+ # https://github.com/rails/rails/commit/945fdd76925c9f615bf016717c4c8db2b2955357#diff-fc90ec41ef75be8b2259526fe1a8b663
+ module Arguments
+ include ActiveJob::Arguments
+ extend self
+
+ private
+
+ def serialize_argument(argument)
+ case argument
+ when -> (arg) { arg.respond_to?(:permitted?) }
+ serialize_hash(argument.to_h).tap do |result|
+ result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
+ end
+ else
+ super
+ end
+ end
+ end
+
+ # Make sure we remove this patch starting with Rails 6.0.
+ if Rails.version.start_with?('6.0')
+ raise <<~MSG
+ Please remove the patch `Arguments` module and use `ActiveJob::Arguments` again.
+ MSG
end
end
end
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index 2965f3b1150..f6e98746055 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -17,7 +17,7 @@ class NamespacelessProjectDestroyWorker
return
end
- return if project.namespace # Reject doing anything for projects that *do* have a namespace
+ return if project.namespace # Reject doing anything for projects that *do* have a namespace
project.team.truncate
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
index 07676011b2a..9c5161fd55a 100644
--- a/app/workers/object_pool/join_worker.rb
+++ b/app/workers/object_pool/join_worker.rb
@@ -5,14 +5,13 @@ module ObjectPool
include ApplicationWorker
include ObjectPoolQueue
- def perform(pool_id, project_id)
- pool = PoolRepository.find_by_id(pool_id)
- return unless pool&.joinable?
-
+ # The use of pool id is deprecated. Keeping the argument allows old jobs to
+ # still be performed.
+ def perform(_pool_id, project_id)
project = Project.find_by_id(project_id)
- return unless project
+ return unless project&.pool_repository&.joinable?
- pool.link_repository(project.repository)
+ project.link_pool_repository
Projects::HousekeepingService.new(project).execute
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index c2fbfd2b3a5..0ddad43b8d5 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -30,6 +30,6 @@ class PipelineMetricsWorker
# rubocop: enable CodeReuse/ActiveRecord
def merge_requests(pipeline)
- pipeline.merge_requests.map(&:id)
+ pipeline.merge_requests_as_head_pipeline.map(&:id)
end
end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 72a1733a2a8..bbd4ab159e4 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,7 +3,7 @@
class PostReceive
include ApplicationWorker
- def perform(gl_repository, identifier, changes)
+ def perform(gl_repository, identifier, changes, push_options = [])
project, is_wiki = Gitlab::GlRepository.parse(gl_repository)
if project.nil?
@@ -15,7 +15,7 @@ class PostReceive
# Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's.
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
- post_received = Gitlab::GitPostReceive.new(project, identifier, changes)
+ post_received = Gitlab::GitPostReceive.new(project, identifier, changes, push_options)
if is_wiki
process_wiki_changes(post_received)
@@ -38,9 +38,21 @@ class PostReceive
post_received.changes_refs do |oldrev, newrev, ref|
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitTagPushService.new(
+ post_received.project,
+ @user,
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref,
+ push_options: post_received.push_options).execute
elsif Gitlab::Git.branch_ref?(ref)
- GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitPushService.new(
+ post_received.project,
+ @user,
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref,
+ push_options: post_received.push_options).execute
end
changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref)
diff --git a/app/workers/project_daily_statistics_worker.rb b/app/workers/project_daily_statistics_worker.rb
new file mode 100644
index 00000000000..101f5c28459
--- /dev/null
+++ b/app/workers/project_daily_statistics_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectDailyStatisticsWorker
+ include ApplicationWorker
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+
+ return unless project&.repository&.exists?
+
+ Projects::FetchStatisticsIncrementService.new(project).execute
+ end
+end
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
deleted file mode 100644
index 4c6339f7701..00000000000
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class ProjectMigrateHashedStorageWorker
- include ApplicationWorker
-
- LEASE_TIMEOUT = 30.seconds.to_i
-
- # rubocop: disable CodeReuse/ActiveRecord
- def perform(project_id, old_disk_path = nil)
- project = Project.find_by(id: project_id)
- return if project.nil? || project.pending_delete?
-
- uuid = lease_for(project_id).try_obtain
- if uuid
- ::Projects::HashedStorageMigrationService.new(project, old_disk_path || project.full_path, logger: logger).execute
- else
- false
- end
- rescue => ex
- cancel_lease_for(project_id, uuid) if uuid
- raise ex
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def lease_for(project_id)
- Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT)
- end
-
- private
-
- def lease_key(project_id)
- "project_migrate_hashed_storage_worker:#{project_id}"
- end
-
- def cancel_lease_for(project_id, uuid)
- Gitlab::ExclusiveLease.cancel(lease_key(project_id), uuid)
- end
-end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 96ff8cd6222..9ec8bcca4f3 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -6,13 +6,13 @@ class ReactiveCachingWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(class_name, id, *args)
klass = begin
- Kernel.const_get(class_name)
+ class_name.constantize
rescue NameError
nil
end
return unless klass
- klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args)
+ klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
index 70c2e857d09..5bafe8e2046 100644
--- a/app/workers/remote_mirror_notification_worker.rb
+++ b/app/workers/remote_mirror_notification_worker.rb
@@ -9,7 +9,10 @@ class RemoteMirrorNotificationWorker
# We check again if there's an error because a newer run since this job was
# fired could've completed successfully.
return unless remote_mirror && remote_mirror.last_error.present?
+ return if remote_mirror.error_notification_sent?
NotificationService.new.remote_mirror_update_failed(remote_mirror)
+
+ remote_mirror.after_sent_notification
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 7eae07d3f6b..a9b88a133be 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -15,19 +15,19 @@ class RepositoryForkWorker
return target_project.import_state.mark_as_failed(_('Source project cannot be found.'))
end
- fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
+ fork_repository(target_project, source_project)
end
private
- def fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ def fork_repository(target_project, source_project)
return unless start_fork(target_project)
Gitlab::Metrics.add_event(:fork_repository)
- result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path,
- target_project.repository_storage, target_project.disk_path)
- raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result
+ result = gitlab_shell.fork_repository(source_project, target_project)
+
+ raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}" unless result
target_project.after_import
end
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
deleted file mode 100644
index fa76fbac55c..00000000000
--- a/app/workers/storage_migrator_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class StorageMigratorWorker
- include ApplicationWorker
-
- def perform(start, finish)
- migrator = Gitlab::HashedStorage::Migrator.new
- migrator.bulk_migrate(start, finish)
- end
-end
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index e8494ffa002..4ec2b9d8fbe 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -7,25 +7,8 @@ class UpdateHeadPipelineForMergeRequestWorker
queue_namespace :pipeline_processing
def perform(merge_request_id)
- merge_request = MergeRequest.find(merge_request_id)
-
- sha = merge_request.diff_head_sha
- pipeline = merge_request.all_pipelines(shas: sha).first
-
- return unless pipeline && pipeline.latest?
-
- if merge_request.diff_head_sha != pipeline.sha
- log_error_message_for(merge_request)
-
- return
+ MergeRequest.find_by_id(merge_request_id).try do |merge_request|
+ merge_request.update_head_pipeline
end
-
- merge_request.update_attribute(:head_pipeline_id, pipeline.id)
- end
-
- def log_error_message_for(merge_request)
- Rails.logger.error(
- "Outdated head pipeline for active merge request: id=#{merge_request.id}, source_branch=#{merge_request.source_branch}, diff_head_sha=#{merge_request.diff_head_sha}"
- )
end
end