Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/auth_buttons/dingtalk_64.pngbin0 -> 1623 bytes
-rw-r--r--app/assets/images/emoji.pngbin1219696 -> 1356857 bytes
-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/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/images/emoji@2x.pngbin2977099 -> 3624162 bytes
-rw-r--r--app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql2
-rw-r--r--app/assets/javascripts/access_tokens/index.js2
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue54
-rw-r--r--app/assets/javascripts/admin/deploy_keys/index.js23
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue2
-rw-r--r--app/assets/javascripts/admin/users/index.js2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue6
-rw-r--r--app/assets/javascripts/alert_management/list.js3
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue4
-rw-r--r--app/assets/javascripts/alerts_settings/graphql.js1
-rw-r--r--app/assets/javascripts/analytics/devops_report/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score.vue (renamed from app/assets/javascripts/analytics/devops_report/components/devops_score.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue (renamed from app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue (renamed from app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_reports/devops_score.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score.js)0
-rw-r--r--app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js)0
-rw-r--r--app/assets/javascripts/analytics/shared/graphql/projects.query.graphql2
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue4
-rw-r--r--app/assets/javascripts/analytics/usage_trends/index.js2
-rw-r--r--app/assets/javascripts/api/namespaces_api.js13
-rw-r--r--app/assets/javascripts/artifacts_settings/index.js2
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue133
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue6
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue10
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue4
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue15
-rw-r--r--app/assets/javascripts/blob/components/table_contents.vue5
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue4
-rw-r--r--app/assets/javascripts/boards/boards_util.js4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue36
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue160
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue82
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue33
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue88
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue53
-rw-r--r--app/assets/javascripts/boards/components/new_board_button.vue47
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue6
-rw-r--r--app/assets/javascripts/boards/graphql.js1
-rw-r--r--app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_scope.fragment.graphql6
-rw-r--r--app/assets/javascripts/boards/graphql/group_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/project_board.query.graphql9
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js8
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js5
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js19
-rw-r--r--app/assets/javascripts/boards/new_board.js29
-rw-r--r--app/assets/javascripts/boards/stores/actions.js95
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/breadcrumb.js18
-rw-r--r--app/assets/javascripts/chronic_duration.js417
-rw-r--r--app/assets/javascripts/ci_lint/index.js4
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue1
-rw-r--r--app/assets/javascripts/clusters/agents/index.js2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue32
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js4
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue81
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue162
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue36
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue54
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue44
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue76
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_main_view.vue73
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_view_all.vue218
-rw-r--r--app/assets/javascripts/clusters_list/components/install_agent_modal.vue52
-rw-r--r--app/assets/javascripts/clusters_list/constants.js94
-rw-r--r--app/assets/javascripts/clusters_list/graphql/cache_update.js29
-rw-r--r--app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql10
-rw-r--r--app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql4
-rw-r--r--app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql12
-rw-r--r--app/assets/javascripts/clusters_list/index.js4
-rw-r--r--app/assets/javascripts/clusters_list/load_agents.js44
-rw-r--r--app/assets/javascripts/clusters_list/load_clusters.js7
-rw-r--r--app/assets/javascripts/clusters_list/load_main_view.js50
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js16
-rw-r--r--app/assets/javascripts/clusters_list/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/clusters_list/store/mutations.js5
-rw-r--r--app/assets/javascripts/clusters_list/store/state.js7
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js10
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_alert.vue33
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_error.vue31
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue22
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue4
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js12
-rw-r--r--app/assets/javascripts/content_editor/extensions/description_list.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/details.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js36
-rw-r--r--app/assets/javascripts/content_editor/extensions/frontmatter.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/horizontal_rule.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/math_inline.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/subscript.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/superscript.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/table.js43
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_of_contents.js21
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js6
-rw-r--r--app/assets/javascripts/content_editor/services/feature_flags.js3
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js16
-rw-r--r--app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js23
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js10
-rw-r--r--app/assets/javascripts/contextual_sidebar.js5
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js40
-rw-r--r--app/assets/javascripts/crm/components/contacts_root.vue80
-rw-r--r--app/assets/javascripts/crm/components/organizations_root.vue71
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql22
-rw-r--r--app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql15
-rw-r--r--app/assets/javascripts/crm/contacts_bundle.js27
-rw-r--r--app/assets/javascripts/crm/organizations_bundle.js27
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/metric_popover.vue61
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue49
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js49
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js69
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js60
-rw-r--r--app/assets/javascripts/deploy_tokens/components/revoke_button.vue2
-rw-r--r--app/assets/javascripts/design_management/graphql.js1
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue18
-rw-r--r--app/assets/javascripts/diffs/components/app.vue15
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_comment_cell.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue31
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue15
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue2
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js34
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js6
-rw-r--r--app/assets/javascripts/diffs/utils/diff_line.js10
-rw-r--r--app/assets/javascripts/diffs/utils/discussions.js76
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js41
-rw-r--r--app/assets/javascripts/editor/constants.js36
-rw-r--r--app/assets/javascripts/editor/extensions/example_source_editor_extension.js116
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js39
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js293
-rw-r--r--app/assets/javascripts/editor/schema/ci.json49
-rw-r--r--app/assets/javascripts/editor/source_editor_extension.js17
-rw-r--r--app/assets/javascripts/editor/source_editor_instance.js271
-rw-r--r--app/assets/javascripts/emoji/index.js1
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue4
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue69
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue47
-rw-r--r--app/assets/javascripts/environments/components/rollback_modal_manager.vue5
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js2
-rw-r--r--app/assets/javascripts/environments/graphql/client.js25
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql7
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js50
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql35
-rw-r--r--app/assets/javascripts/environments/index.js69
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js6
-rw-r--r--app/assets/javascripts/environments/new_index.js38
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue5
-rw-r--r--app/assets/javascripts/experimentation/utils.js23
-rw-r--r--app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue1
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js24
-rw-r--r--app/assets/javascripts/google_cloud/components/app.vue50
-rw-r--r--app/assets/javascripts/google_cloud/components/incubation_banner.vue44
-rw-r--r--app/assets/javascripts/google_cloud/components/service_accounts.vue65
-rw-r--r--app/assets/javascripts/google_cloud/index.js11
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql4
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js8
-rw-r--r--app/assets/javascripts/group.js4
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue41
-rw-r--r--app/assets/javascripts/group_settings/constants.js5
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js21
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue5
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue4
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/shared/commit_message_field.vue137
-rw-r--r--app/assets/javascripts/ide/constants.js2
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue33
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue310
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue84
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js406
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql23
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql23
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql13
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js74
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js87
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js35
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql52
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/services/status_poller.js39
-rw-r--r--app/assets/javascripts/import_entities/import_groups/utils.js23
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue6
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue10
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue11
-rw-r--r--app/assets/javascripts/incidents/constants.js5
-rw-r--r--app/assets/javascripts/incidents/list.js4
-rw-r--r--app/assets/javascripts/init_confirm_danger.js38
-rw-r--r--app/assets/javascripts/integrations/constants.js8
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue10
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue53
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js16
-rw-r--r--app/assets/javascripts/invite_members/components/confetti.vue33
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue232
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue27
-rw-r--r--app/assets/javascripts/invite_members/constants.js127
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js19
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/index.js7
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue29
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue14
-rw-r--r--app/assets/javascripts/issue_show/components/fields/type.vue17
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue7
-rw-r--r--app/assets/javascripts/issue_show/constants.js7
-rw-r--r--app/assets/javascripts/issue_show/incident.js56
-rw-r--r--app/assets/javascripts/issue_show/issue.js7
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue26
-rw-r--r--app/assets/javascripts/issues_list/components/new_issue_dropdown.vue7
-rw-r--r--app/assets/javascripts/issues_list/constants.js28
-rw-r--r--app/assets/javascripts/issues_list/index.js6
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql10
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql22
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/iteration.fragment.graphql6
-rw-r--r--app/assets/javascripts/issues_list/queries/search_projects.query.graphql1
-rw-r--r--app/assets/javascripts/issues_list/service_desk_helper.js8
-rw-r--r--app/assets/javascripts/issues_list/utils.js34
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql2
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js7
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue24
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue26
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue (renamed from app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue85
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue112
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue85
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue36
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue54
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js16
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/utils.js15
-rw-r--r--app/assets/javascripts/jira_import/index.js2
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql2
-rw-r--r--app/assets/javascripts/jira_import/queries/search_project_members.query.graphql2
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue229
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue8
-rw-r--r--app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js4
-rw-r--r--app/assets/javascripts/lib/graphql.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js27
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue47
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js55
-rw-r--r--app/assets/javascripts/lib/utils/constants.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js48
-rw-r--r--app/assets/javascripts/lib/utils/file_upload.js20
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js38
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js6
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue2
-rw-r--r--app/assets/javascripts/members/components/app.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue285
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue324
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue106
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js6
-rw-r--r--app/assets/javascripts/monitoring/utils.js1
-rw-r--r--app/assets/javascripts/mr_notes/index.js4
-rw-r--r--app/assets/javascripts/mr_popover/index.js7
-rw-r--r--app/assets/javascripts/nav/components/responsive_home.vue1
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue1
-rw-r--r--app/assets/javascripts/network/branch_graph.js2
-rw-r--r--app/assets/javascripts/notebook/cells/output/latex.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue90
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue32
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue18
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js56
-rw-r--r--app/assets/javascripts/notes/stores/actions.js46
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js41
-rw-r--r--app/assets/javascripts/packages/list/components/package_search.vue57
-rw-r--r--app/assets/javascripts/packages/list/components/package_title.vue47
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue28
-rw-r--r--app/assets/javascripts/packages/list/components/tokens/package_type_token.vue26
-rw-r--r--app/assets/javascripts/packages/list/constants.js6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue (renamed from app/assets/javascripts/registry/explorer/components/delete_button.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue (renamed from app/assets/javascripts/registry/explorer/components/delete_image.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/details_header.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/image_list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue (renamed from app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue (renamed from app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js (renamed from app/assets/javascripts/registry/explorer/constants/common.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js (renamed from app/assets/javascripts/registry/explorer/constants/details.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js (renamed from app/assets/javascripts/registry/explorer/constants/expiration_policies.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js (renamed from app/assets/javascripts/registry/explorer/constants/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js (renamed from app/assets/javascripts/registry/explorer/constants/list.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js (renamed from app/assets/javascripts/registry/explorer/constants/quick_start.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql (renamed from app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js (renamed from app/assets/javascripts/registry/explorer/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue (renamed from app/assets/javascripts/registry/explorer/pages/details.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue (renamed from app/assets/javascripts/registry/explorer/pages/index.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue (renamed from app/assets/javascripts/registry/explorer/pages/list.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js (renamed from app/assets/javascripts/registry/explorer/router.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue87
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue49
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue52
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue86
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue62
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue116
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue100
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js63
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql31
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/utils.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/deploy_keys/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js8
-rw-r--r--app/assets/javascripts/pages/groups/crm/contacts/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/crm/organizations/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/components/app.vue2
-rw-r--r--app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue2
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js12
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js11
-rw-r--r--app/assets/javascripts/pages/groups/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/environments/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue6
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js6
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue14
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue1
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js1
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue6
-rw-r--r--app/assets/javascripts/pages/projects/project.js8
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue17
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue7
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index.js (renamed from app/assets/javascripts/pages/projects/work_items/index/index.js)0
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js7
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue18
-rw-r--r--app/assets/javascripts/pages/shared/wikis/constants.js5
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js11
-rw-r--r--app/assets/javascripts/pages/users/terms/index/index.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue16
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue23
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue49
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue18
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue52
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue56
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue53
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue7
-rw-r--r--app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue83
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js17
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql5
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js23
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js11
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue51
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue70
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue58
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql3
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_shared_client.js1
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue4
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/project_visibility.js62
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue10
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js7
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue58
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue7
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue27
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql3
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js4
-rw-r--r--app/assets/javascripts/projects/project_delete_button.js16
-rw-r--r--app/assets/javascripts/projects/project_new.js7
-rw-r--r--app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue92
-rw-r--r--app/assets/javascripts/projects/settings/topics/index.js51
-rw-r--r--app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue60
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue115
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_table.vue62
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue35
-rw-r--r--app/assets/javascripts/projects/storage_counter/constants.js12
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/utils.js4
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/projects/upload_file.js33
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment.js33
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment_tracking.js9
-rw-r--r--app/assets/javascripts/ref/constants.js4
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/index.js15
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue87
-rw-r--r--app/assets/javascripts/related_merge_requests/store/actions.js4
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue7
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql2
-rw-r--r--app/assets/javascripts/releases/mount_index.js1
-rw-r--r--app/assets/javascripts/releases/mount_show.js7
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue19
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue13
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue19
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue27
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/repository/graphql.js1
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js3
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql1
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js7
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue87
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js29
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue4
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue40
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_summary_cell.vue27
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_type_cell.vue47
-rw-r--r--app/assets/javascripts/runner/components/helpers/masked_value.vue60
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue112
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token.vue83
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue113
-rw-r--r--app/assets/javascripts/runner/components/runner_contacted_state_badge.vue69
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue39
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue108
-rw-r--r--app/assets/javascripts/runner/components/runner_paused_badge.vue (renamed from app/assets/javascripts/runner/components/runner_state_paused_badge.vue)0
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue114
-rw-r--r--app/assets/javascripts/runner/components/runner_state_locked_badge.vue25
-rw-r--r--app/assets/javascripts/runner/components/runner_tag.vue35
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue10
-rw-r--r--app/assets/javascripts/runner/components/runner_type_alert.vue5
-rw-r--r--app/assets/javascripts/runner/components/runner_type_badge.vue5
-rw-r--r--app/assets/javascripts/runner/components/runner_type_tabs.vue66
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js28
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/type_token_config.js20
-rw-r--r--app/assets/javascripts/runner/constants.js16
-rw-r--r--app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql14
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql3
-rw-r--r--app/assets/javascripts/runner/graphql/runner_update.mutation.graphql3
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue24
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js9
-rw-r--r--app/assets/javascripts/runner/runner_details/index.js7
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js88
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/constants/state_filter_data.js2
-rw-r--r--app/assets/javascripts/search/store/actions.js10
-rw-r--r--app/assets/javascripts/search/store/constants.js5
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/search/store/mutations.js3
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/search/store/utils.js12
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue22
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js51
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue6
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue74
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue17
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue8
-rw-r--r--app/assets/javascripts/sidebar/constants.js28
-rw-r--r--app/assets/javascripts/sidebar/graphql.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js4
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js12
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js43
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js12
-rw-r--r--app/assets/javascripts/snippets/index.js1
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js1
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue2
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/terms/components/app.vue117
-rw-r--r--app/assets/javascripts/terms/index.js23
-rw-r--r--app/assets/javascripts/token_access/index.js2
-rw-r--r--app/assets/javascripts/user_lists/components/user_lists_table.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue28
-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.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue254
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue7
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue104
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue60
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue64
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue184
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/directives/validation.js6
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/provider.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue10
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js1
-rw-r--r--app/assets/javascripts/work_items/components/app.vue10
-rw-r--r--app/assets/javascripts/work_items/constants.js3
-rw-r--r--app/assets/javascripts/work_items/graphql/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js55
-rw-r--r--app/assets/javascripts/work_items/graphql/resolvers.js0
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql38
-rw-r--r--app/assets/javascripts/work_items/graphql/widget.fragment.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql16
-rw-r--r--app/assets/javascripts/work_items/index.js4
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue48
-rw-r--r--app/assets/javascripts/work_items/router/index.js14
-rw-r--r--app/assets/javascripts/work_items/router/routes.js8
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss54
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss32
-rw-r--r--app/assets/stylesheets/framework/kbd.scss16
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss6
-rw-r--r--app/assets/stylesheets/framework/terms.scss60
-rw-r--r--app/assets/stylesheets/highlight/common.scss9
-rw-r--r--app/assets/stylesheets/mailer.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/terms.scss64
-rw-r--r--app/assets/stylesheets/pages/clusters.scss29
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss9
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/startup/_cloaking.scss2
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss12
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss2
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss2
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss22
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_dark.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss2
-rw-r--r--app/assets/stylesheets/utilities.scss16
2521 files changed, 11875 insertions, 5329 deletions
diff --git a/app/assets/images/auth_buttons/dingtalk_64.png b/app/assets/images/auth_buttons/dingtalk_64.png
new file mode 100644
index 00000000000..77b3fa752bc
--- /dev/null
+++ b/app/assets/images/auth_buttons/dingtalk_64.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 723c2c3f4c8..bc5041a165b 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
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/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/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index 987279c13cc..8fe724329bf 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
index 60110437ecd..09278e1776a 100644
--- a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
+++ b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getProjects(
+query accessTokensGetProjects(
$search: String = ""
$after: String = ""
$first: Int = null
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index 2cd3a8f12ee..7f5f0403de6 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -49,7 +49,7 @@ export const initProjectsField = () => {
{ default: createDefaultClient },
]) => {
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
new file mode 100644
index 00000000000..97a5a2f2f32
--- /dev/null
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlTable, GlButton } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+
+export default {
+ name: 'DeployKeysTable',
+ i18n: {
+ pageTitle: __('Public deploy keys'),
+ newDeployKeyButtonText: __('New deploy key'),
+ },
+ fields: [
+ {
+ key: 'title',
+ label: __('Title'),
+ },
+ {
+ key: 'fingerprint',
+ label: __('Fingerprint'),
+ },
+ {
+ key: 'projects',
+ label: __('Projects with write access'),
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ },
+ {
+ key: 'actions',
+ label: __('Actions'),
+ },
+ ],
+ components: {
+ GlTable,
+ GlButton,
+ },
+ inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-5">
+ <h4 class="gl-m-0">
+ {{ $options.i18n.pageTitle }}
+ </h4>
+ <gl-button variant="confirm" :href="createPath">{{
+ $options.i18n.newDeployKeyButtonText
+ }}</gl-button>
+ </div>
+ <gl-table :fields="$options.fields" data-testid="deploy-keys-list" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/deploy_keys/index.js b/app/assets/javascripts/admin/deploy_keys/index.js
new file mode 100644
index 00000000000..d86de4229de
--- /dev/null
+++ b/app/assets/javascripts/admin/deploy_keys/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import DeployKeysTable from './components/table.vue';
+
+export const initAdminDeployKeysTable = () => {
+ const el = document.getElementById('js-admin-deploy-keys-table');
+
+ if (!el) return false;
+
+ const { editPath, deletePath, createPath, emptyStateSvgPath } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ editPath,
+ deletePath,
+ createPath,
+ emptyStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(DeployKeysTable);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index ed90343777d..e949498c55b 100644
--- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -138,7 +138,7 @@ export default {
/>
</form>
<template #modal-footer>
- <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
+ <gl-button @click="onCancel">{{ __('Cancel') }}</gl-button>
<gl-button
:disabled="!canSubmit"
category="secondary"
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 852b253d25a..0c485d2a239 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -15,7 +15,7 @@ import {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const initApp = (el, component, userPropKey, props = {}) => {
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index e59d7fc058a..79a6bac3ba7 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -17,7 +17,6 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
-import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import {
tdClass,
thClass,
@@ -26,7 +25,6 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_STATUS_TABS, SEVERITY_LEVELS, trackAlertListViewsOptions } from '../constants';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
@@ -98,7 +96,6 @@ export default {
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
- AlertsDeprecationWarning,
GlAlert,
GlLoadingIcon,
GlTable,
@@ -115,7 +112,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'textQuery', 'assigneeUsernameQuery', 'populatingAlertsHelpUrl'],
apollo: {
alerts: {
@@ -277,8 +273,6 @@ export default {
</gl-sprintf>
</gl-alert>
- <alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
-
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index 57d1f135606..b23f8a8eba4 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -23,7 +23,6 @@ export default () => {
assigneeUsernameQuery,
alertManagementEnabled,
userCanEnableAlertManagement,
- hasManagedPrometheus,
} = domEl.dataset;
const apolloProvider = new VueApollo({
@@ -39,7 +38,6 @@ export default () => {
return defaultDataIdFromObject(object);
},
},
- assumeImmutableResults: true,
},
),
});
@@ -66,7 +64,6 @@ export default () => {
alertManagementEnabled: parseBoolean(alertManagementEnabled),
trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
- hasManagedPrometheus: parseBoolean(hasManagedPrometheus),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index a5f7b84446f..6b5aac57f1c 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -16,6 +16,7 @@ import {
import * as Sentry from '@sentry/browser';
import { isEqual, isEmpty, omit } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import {
integrationTypes,
integrationSteps,
@@ -129,6 +130,7 @@ export default {
name: true,
apiUrl: true,
},
+ pricingLink: `${PROMO_URL}/pricing`,
};
},
computed: {
@@ -436,7 +438,7 @@ export default {
disabled="true"
class="gl-display-inline-block gl-my-4"
:message="$options.i18n.integrationFormSteps.selectType.enterprise"
- link="https://about.gitlab.com/pricing"
+ :link="pricingLink"
data-testid="multi-integrations-not-supported"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js
index 15862f4034a..b64e2e3eefa 100644
--- a/app/assets/javascripts/alerts_settings/graphql.js
+++ b/app/assets/javascripts/alerts_settings/graphql.js
@@ -59,6 +59,5 @@ export default new VueApollo({
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/analytics/devops_report/constants.js b/app/assets/javascripts/analytics/devops_report/constants.js
deleted file mode 100644
index b395d7eb464..00000000000
--- a/app/assets/javascripts/analytics/devops_report/constants.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { __ } from '~/locale';
-
-export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
-
-export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
-
-export const INTRO_BANNER_BODY = __(
- 'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
-);
-
-export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
index 238081cc3c0..238081cc3c0 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
index e594b4e360a..e594b4e360a 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
index 400326e41e1..400326e41e1 100644
--- a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
diff --git a/app/assets/javascripts/analytics/devops_reports/constants.js b/app/assets/javascripts/analytics/devops_reports/constants.js
new file mode 100644
index 00000000000..6091fcb5724
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_reports/constants.js
@@ -0,0 +1,11 @@
+import { __ } from '~/locale';
+
+export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
+
+export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Reports');
+
+export const INTRO_BANNER_BODY = __(
+ 'Your DevOps Reports give an overview of how you are using GitLab from a feature perspective. Use them to view how you compare with other organizations, and how your teams compare against each other.',
+);
+
+export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_reports/devops_score.js
index 0bf98b65ed5..0bf98b65ed5 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score.js
+++ b/app/assets/javascripts/analytics/devops_reports/devops_score.js
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js b/app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
index eb2992422a4..eb2992422a4 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
+++ b/app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
diff --git a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
index 63e95d6804c..b870ed4dcbf 100644
--- a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
+++ b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
@@ -1,4 +1,4 @@
-query getGroupProjects(
+query analyticsGetGroupProjects(
$groupFullPath: ID!
$search: String!
$first: Int!
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 1eb4832a2a3..63ec40d4ec6 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -3,7 +3,7 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createFlash from '~/flash';
import { number } from '~/lib/utils/unit_format';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql';
const defaultPrecision = 0;
@@ -52,7 +52,7 @@ export default {
mergeRequests: s__('UsageTrends|Merge requests'),
pipelines: s__('UsageTrends|Pipelines'),
},
- loadCountsError: s__('Could not load usage counts. Please refresh the page to try again.'),
+ loadCountsError: __('Could not load usage counts. Please refresh the page to try again.'),
},
};
</script>
diff --git a/app/assets/javascripts/analytics/usage_trends/index.js b/app/assets/javascripts/analytics/usage_trends/index.js
index 3e85832edcf..d1880b09f15 100644
--- a/app/assets/javascripts/analytics/usage_trends/index.js
+++ b/app/assets/javascripts/analytics/usage_trends/index.js
@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/api/namespaces_api.js b/app/assets/javascripts/api/namespaces_api.js
new file mode 100644
index 00000000000..166a95b749a
--- /dev/null
+++ b/app/assets/javascripts/api/namespaces_api.js
@@ -0,0 +1,13 @@
+import { buildApiUrl } from '~/api/api_utils';
+import axios from '~/lib/utils/axios_utils';
+
+const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
+
+export function getGroupPathAvailability(groupPath, parentId, axiosOptions = {}) {
+ const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
+
+ return axios.get(url, {
+ params: { parent_id: parentId, ...axiosOptions.params },
+ ...axiosOptions,
+ });
+}
diff --git a/app/assets/javascripts/artifacts_settings/index.js b/app/assets/javascripts/artifacts_settings/index.js
index 5c9f1c3129c..531b42bc185 100644
--- a/app/assets/javascripts/artifacts_settings/index.js
+++ b/app/assets/javascripts/artifacts_settings/index.js
@@ -6,7 +6,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default (containerId = 'js-artifacts-settings-app') => {
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
index 0b748f18cb2..484c6524d0e 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -1,26 +1,46 @@
<script>
-import { GlFormInput, GlFormGroup, GlButton, GlForm } from '@gitlab/ui';
+import { GlFormInput, GlFormGroup, GlButton, GlForm, GlModal } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
export const i18n = {
currentPassword: __('Current password'),
+ confirmTitle: __('Are you sure?'),
confirmWebAuthn: __(
- 'Are you sure? This will invalidate your registered applications and U2F / WebAuthn devices.',
+ 'This will invalidate your registered applications and U2F / WebAuthn devices.',
),
- confirm: __('Are you sure? This will invalidate your registered applications and U2F devices.'),
+ confirm: __('This will invalidate your registered applications and U2F devices.'),
disableTwoFactor: __('Disable two-factor authentication'),
+ disable: __('Disable'),
+ cancel: __('Cancel'),
regenerateRecoveryCodes: __('Regenerate recovery codes'),
+ currentPasswordInvalidFeedback: __('Please enter your current password.'),
};
export default {
name: 'ManageTwoFactorForm',
i18n,
+ modalId: 'manage-two-factor-auth-confirm-modal',
+ modalActions: {
+ primary: {
+ text: i18n.disable,
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ secondary: {
+ text: i18n.cancel,
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
components: {
GlForm,
GlFormInput,
GlFormGroup,
GlButton,
+ GlModal,
},
inject: [
'webauthnEnabled',
@@ -32,8 +52,11 @@ export default {
],
data() {
return {
- method: '',
- action: '#',
+ method: null,
+ action: null,
+ currentPassword: '',
+ currentPasswordState: null,
+ showConfirmModal: false,
};
},
computed: {
@@ -46,9 +69,34 @@ export default {
},
},
methods: {
- handleFormSubmit(event) {
- this.method = event.submitter.dataset.formMethod;
- this.action = event.submitter.dataset.formAction;
+ submitForm() {
+ this.$refs.form.$el.submit();
+ },
+ async handleSubmitButtonClick({ method, action, confirm = false }) {
+ this.method = method;
+ this.action = action;
+
+ if (this.isCurrentPasswordRequired && this.currentPassword === '') {
+ this.currentPasswordState = false;
+
+ return;
+ }
+
+ this.currentPasswordState = null;
+
+ if (confirm) {
+ this.showConfirmModal = true;
+
+ return;
+ }
+
+ // Wait for form action and method to be updated
+ await this.$nextTick();
+
+ this.submitForm();
+ },
+ handleModalPrimary() {
+ this.submitForm();
},
},
csrf,
@@ -57,10 +105,11 @@ export default {
<template>
<gl-form
- class="gl-display-inline-block"
+ ref="form"
+ class="gl-sm-display-inline-block"
method="post"
:action="action"
- @submit="handleFormSubmit($event)"
+ @submit.prevent
>
<input type="hidden" name="_method" data-testid="test-2fa-method-field" :value="method" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
@@ -69,35 +118,59 @@ export default {
v-if="isCurrentPasswordRequired"
:label="$options.i18n.currentPassword"
label-for="current-password"
+ :state="currentPasswordState"
+ :invalid-feedback="$options.i18n.currentPasswordInvalidFeedback"
>
<gl-form-input
id="current-password"
+ v-model="currentPassword"
type="password"
name="current_password"
- required
+ :state="currentPasswordState"
data-qa-selector="current_password_field"
/>
</gl-form-group>
- <gl-button
- type="submit"
- class="btn-danger gl-mr-3 gl-display-inline-block"
- data-testid="test-2fa-disable-button"
- variant="danger"
- :data-confirm="confirmText"
- :data-form-action="profileTwoFactorAuthPath"
- :data-form-method="profileTwoFactorAuthMethod"
- >
- {{ $options.i18n.disableTwoFactor }}
- </gl-button>
- <gl-button
- type="submit"
- class="gl-display-inline-block"
- data-testid="test-2fa-regenerate-codes-button"
- :data-form-action="codesProfileTwoFactorAuthPath"
- :data-form-method="codesProfileTwoFactorAuthMethod"
+ <div class="gl-display-flex gl-flex-wrap">
+ <gl-button
+ type="submit"
+ class="gl-sm-mr-3 gl-w-full gl-sm-w-auto"
+ data-testid="test-2fa-disable-button"
+ variant="danger"
+ @click.prevent="
+ handleSubmitButtonClick({
+ method: profileTwoFactorAuthMethod,
+ action: profileTwoFactorAuthPath,
+ confirm: true,
+ })
+ "
+ >
+ {{ $options.i18n.disableTwoFactor }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ class="gl-mt-3 gl-sm-mt-0 gl-w-full gl-sm-w-auto"
+ data-testid="test-2fa-regenerate-codes-button"
+ @click.prevent="
+ handleSubmitButtonClick({
+ method: codesProfileTwoFactorAuthMethod,
+ action: codesProfileTwoFactorAuthPath,
+ })
+ "
+ >
+ {{ $options.i18n.regenerateRecoveryCodes }}
+ </gl-button>
+ </div>
+ <gl-modal
+ v-model="showConfirmModal"
+ :modal-id="$options.modalId"
+ size="sm"
+ :title="$options.i18n.confirmTitle"
+ :action-primary="$options.modalActions.primary"
+ :action-secondary="$options.modalActions.secondary"
+ @primary="handleModalPrimary"
>
- {{ $options.i18n.regenerateRecoveryCodes }}
- </gl-button>
+ {{ confirmText }}
+ </gl-modal>
</gl-form>
</template>
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 825807e833e..0303930de5d 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -2,7 +2,7 @@
import { GlSprintf, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import createFlash from '~/flash';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
import BadgeList from './badge_list.vue';
@@ -25,13 +25,13 @@ export default {
...mapState(['badgeInModal', 'isEditing']),
primaryProps() {
return {
- text: s__('Delete badge'),
+ text: __('Delete badge'),
attributes: [{ category: 'primary' }, { variant: 'danger' }],
};
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
},
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index f5e3bab6ff0..918519f386b 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import NoteableNote from '~/notes/components/noteable_note.vue';
import PublishButton from './publish_button.vue';
@@ -10,6 +10,9 @@ export default {
PublishButton,
GlButton,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
draft: {
type: Object,
@@ -72,6 +75,9 @@ export default {
}
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji'],
+ },
};
</script>
<template>
@@ -104,8 +110,8 @@ export default {
<template v-if="!isEditingDraft">
<div
v-if="draftCommands"
+ v-safe-html:[$options.safeHtmlConfig]="draftCommands"
class="referenced-commands draft-note-commands"
- v-html="draftCommands /* eslint-disable-line vue/no-v-html */"
></div>
<p class="draft-note-actions d-flex">
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 91b3b6a685c..e90c29e939f 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -15,14 +15,14 @@ export default {
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
},
methods: {
- ...mapActions('diffs', ['toggleActiveFileByHash']),
+ ...mapActions('diffs', ['setCurrentFileHash']),
...mapActions('batchComments', ['scrollToDraft']),
isLast(index) {
return index === this.sortedDrafts.length - 1;
},
async onClickDraft(draft) {
if (this.viewDiffsFileByFile && draft.file_hash) {
- await this.toggleActiveFileByHash(draft.file_hash);
+ await this.setCurrentFileHash(draft.file_hash);
}
await this.scrollToDraft(draft);
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index 213e026c41f..e3e43ea3a0e 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -65,7 +65,7 @@ export default {
};
</script>
<template>
- <div class="blob-viewer" :data-type="activeViewer.type">
+ <div class="blob-viewer" :data-type="activeViewer.type" :data-loaded="!loading">
<gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
<template v-else>
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 4742b4ae4b4..933ad448c77 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -3,12 +3,14 @@ import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
+import TableOfContents from './table_contents.vue';
export default {
components: {
ViewerSwitcher,
DefaultActions,
BlobFilepath,
+ TableOfContents,
},
props: {
blob: {
@@ -70,11 +72,14 @@ export default {
</script>
<template>
<div class="js-file-title file-title-flex-parent">
- <blob-filepath :blob="blob">
- <template #filepath-prepend>
- <slot name="prepend"></slot>
- </template>
- </blob-filepath>
+ <div class="gl-display-flex">
+ <table-of-contents class="gl-pr-2" />
+ <blob-filepath :blob="blob">
+ <template #filepath-prepend>
+ <slot name="prepend"></slot>
+ </template>
+ </blob-filepath>
+ </div>
<div class="gl-display-none gl-sm-display-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
diff --git a/app/assets/javascripts/blob/components/table_contents.vue b/app/assets/javascripts/blob/components/table_contents.vue
index 78ecb82f2cd..07da262ec9a 100644
--- a/app/assets/javascripts/blob/components/table_contents.vue
+++ b/app/assets/javascripts/blob/components/table_contents.vue
@@ -18,11 +18,12 @@ export default {
},
mounted() {
this.blobViewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ const blobViewerAttr = (attr) => this.blobViewer.getAttribute(attr);
this.observer = new MutationObserver(() => {
- if (this.blobViewer.classList.contains('hidden')) {
+ if (this.blobViewer.classList.contains('hidden') || blobViewerAttr('data-type') !== 'rich') {
this.isHidden = true;
- } else if (this.blobViewer.getAttribute('data-loaded') === 'true') {
+ } else if (blobViewerAttr('data-loaded') === 'true') {
this.isHidden = false;
this.generateHeaders();
}
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index a3278f8bde2..e75aa523ed0 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
@@ -69,7 +69,7 @@ export default {
},
},
i18n: {
- modalTitle: s__("That's it, well done!"),
+ modalTitle: __("That's it, well done!"),
pipelinesButton: s__('MR widget|See your pipeline in action'),
mergeRequestButton: s__('MR widget|Back to the Merge request'),
bodyMessage: s__(
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index c10241d00d7..e6c91c7ac1f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,4 +1,5 @@
import { sortBy, cloneDeep } from 'lodash';
+import { isGid } from '~/graphql_shared/utils';
import { ListType, MilestoneIDs } from './constants';
export function getMilestone() {
@@ -95,6 +96,9 @@ export function fullMilestoneId(id) {
}
export function fullLabelId(label) {
+ if (isGid(label.id)) {
+ return label.id;
+ }
if (label.project_id && label.project_id !== null) {
return `gid://gitlab/ProjectLabel/${label.id}`;
}
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 1e780f9ef84..563bed6a6b8 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,7 +83,7 @@ export default {
:data-item-path="item.referencePath"
data-testid="board_card"
class="board-card gl-p-5 gl-rounded-base"
- @mouseup="toggleIssue($event)"
+ @click="toggleIssue($event)"
>
<board-card-inner :list="list" :item="item" :update-filters="true" />
</li>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 9bbb8a1a1b2..54668c9e88e 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -53,6 +54,9 @@ export default {
allowLabelEdit: {
default: false,
},
+ labelsFilterBasePath: {
+ default: '',
+ },
},
inheritAttrs: false,
computed: {
@@ -63,7 +67,7 @@ export default {
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
+ ...mapState(['sidebarType', 'issuableType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -84,7 +88,15 @@ export default {
});
},
attrWorkspacePath() {
- return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
+ return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
+ },
+ labelType() {
+ return this.isGroupBoard ? LabelType.group : LabelType.project;
+ },
+ labelsFilterPath() {
+ return this.isGroupBoard
+ ? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
+ : this.labelsFilterBasePath;
},
},
methods: {
@@ -98,21 +110,19 @@ export default {
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
- handleUpdateSelectedLabels(input) {
+ handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
- iid: this.activeBoardItem.iid,
+ id,
projectPath: this.projectPathForActiveIssue,
- addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
- removeLabelIds: this.activeBoardItem.labels
- .filter((label) => !input.find((selected) => selected.id === label.id))
- .map((label) => label.id),
+ labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
+ labels,
});
},
- handleLabelRemove(input) {
+ handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue,
- removeLabelIds: [input],
+ removeLabelIds: [removeLabelId],
});
},
},
@@ -207,14 +217,14 @@ export default {
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
- :selected-labels="activeBoardItem.labels"
- :labels-select-in-progress="isSettingLabels"
:footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle"
- :labels-filter-base-path="projectPathForActiveIssue"
+ :labels-filter-base-path="labelsFilterPath"
:attr-workspace-path="attrWorkspacePath"
+ workspace-type="project"
:issuable-type="issuableType"
+ :label-create-type="labelType"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 7f242dea644..6e6ada2d109 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -1,6 +1,7 @@
<script>
-import { pickBy } from 'lodash';
+import { pickBy, isEmpty } from 'lodash';
import { mapActions } from 'vuex';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -19,6 +20,11 @@ export default {
type: Array,
required: true,
},
+ eeFilters: {
+ required: false,
+ type: Object,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -26,57 +32,6 @@ export default {
};
},
computed: {
- urlParams() {
- const {
- authorUsername,
- labelName,
- assigneeUsername,
- search,
- milestoneTitle,
- types,
- weight,
- } = this.filterParams;
- let notParams = {};
-
- if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
- notParams = pickBy(
- {
- 'not[label_name][]': this.filterParams.not.labelName,
- 'not[author_username]': this.filterParams.not.authorUsername,
- 'not[assignee_username]': this.filterParams.not.assigneeUsername,
- 'not[types]': this.filterParams.not.types,
- 'not[milestone_title]': this.filterParams.not.milestoneTitle,
- 'not[weight]': this.filterParams.not.weight,
- },
- undefined,
- );
- }
-
- return {
- ...notParams,
- author_username: authorUsername,
- 'label_name[]': labelName,
- assignee_username: assigneeUsername,
- milestone_title: milestoneTitle,
- search,
- types,
- weight,
- };
- },
- },
- methods: {
- ...mapActions(['performSearch']),
- handleFilter(filters) {
- this.filterParams = this.getFilterParams(filters);
-
- updateHistory({
- url: setUrlParams(this.urlParams, window.location.href, true, false, true),
- title: document.title,
- replace: true,
- });
-
- this.performSearch();
- },
getFilteredSearchValue() {
const {
authorUsername,
@@ -86,6 +41,8 @@ export default {
milestoneTitle,
types,
weight,
+ epicId,
+ myReactionEmoji,
} = this.filterParams;
const filteredSearchValue = [];
@@ -133,6 +90,20 @@ export default {
});
}
+ if (myReactionEmoji) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: myReactionEmoji, operator: '=' },
+ });
+ }
+
+ if (epicId) {
+ filteredSearchValue.push({
+ type: 'epic_id',
+ value: { data: epicId, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author_username',
@@ -177,12 +148,89 @@ export default {
});
}
+ if (this.filterParams['not[epicId]']) {
+ filteredSearchValue.push({
+ type: 'epic_id',
+ value: { data: this.filterParams['not[epicId]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[myReactionEmoji]']) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
return filteredSearchValue;
},
+ urlParams() {
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ epicId,
+ myReactionEmoji,
+ } = this.filterParams;
+
+ let notParams = {};
+
+ if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
+ notParams = pickBy(
+ {
+ 'not[label_name][]': this.filterParams.not.labelName,
+ 'not[author_username]': this.filterParams.not.authorUsername,
+ 'not[assignee_username]': this.filterParams.not.assigneeUsername,
+ 'not[types]': this.filterParams.not.types,
+ 'not[milestone_title]': this.filterParams.not.milestoneTitle,
+ 'not[weight]': this.filterParams.not.weight,
+ 'not[epic_id]': this.filterParams.not.epicId,
+ 'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
+ },
+ undefined,
+ );
+ }
+
+ return {
+ ...notParams,
+ author_username: authorUsername,
+ 'label_name[]': labelName,
+ assignee_username: assigneeUsername,
+ milestone_title: milestoneTitle,
+ search,
+ types,
+ weight,
+ epic_id: getIdFromGraphQLId(epicId),
+ my_reaction_emoji: myReactionEmoji,
+ };
+ },
+ },
+ created() {
+ if (!isEmpty(this.eeFilters)) {
+ this.filterParams = this.eeFilters;
+ }
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ handleFilter(filters) {
+ this.filterParams = this.getFilterParams(filters);
+
+ updateHistory({
+ url: setUrlParams(this.urlParams, window.location.href, true, false, true),
+ title: document.title,
+ replace: true,
+ });
+
+ this.performSearch();
+ },
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');
const equalsFilters = filters.filter(
@@ -216,6 +264,12 @@ export default {
case 'weight':
filterParams.weight = filter.value.data;
break;
+ case 'epic_id':
+ filterParams.epicId = filter.value.data;
+ break;
+ case 'my_reaction_emoji':
+ filterParams.myReactionEmoji = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
@@ -243,7 +297,7 @@ export default {
namespace=""
:tokens="tokens"
:search-input-placeholder="$options.i18n.search"
- :initial-filter-value="getFilteredSearchValue()"
+ :initial-filter-value="getFilteredSearchValue"
@onFilter="handleFilter"
/>
</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index e939f0c0ebe..6ad57fd8985 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,10 +2,10 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import { fullLabelId, fullBoardId } from '../boards_util';
+import { fullLabelId } from '../boards_util';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -18,11 +18,11 @@ const boardDefaults = {
name: '',
labels: [],
milestone: {},
- iteration_id: undefined,
+ iteration: {},
assignee: {},
weight: null,
- hide_backlog_list: false,
- hide_closed_list: false,
+ hideBacklogList: false,
+ hideClosedList: false,
};
export default {
@@ -57,39 +57,16 @@ export default {
type: Boolean,
required: true,
},
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
- projectId: {
- type: Number,
- required: false,
- default: 0,
- },
- groupId: {
- type: Number,
- required: false,
- default: 0,
- },
weights: {
type: Array,
required: false,
default: () => [],
},
- enableScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
currentBoard: {
type: Object,
required: true,
@@ -167,17 +144,16 @@ export default {
return destroyBoardMutation;
},
baseMutationVariables() {
- const { board } = this;
- const variables = {
- name: board.name,
- hideBacklogList: board.hide_backlog_list,
- hideClosedList: board.hide_closed_list,
- };
+ const {
+ board: { name, hideBacklogList, hideClosedList, id },
+ } = this;
- return board.id
+ const variables = { name, hideBacklogList, hideClosedList };
+
+ return id
? {
...variables,
- id: fullBoardId(board.id),
+ id,
}
: {
...variables,
@@ -191,11 +167,13 @@ export default {
assigneeId: this.board.assignee?.id
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
+ // Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
milestoneId: this.board.milestone?.id
- ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
+ ? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id))
: null,
- iterationId: this.board.iteration_id
- ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
+ // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
+ iterationId: this.board.iteration?.id
+ ? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id))
: null,
};
},
@@ -249,7 +227,7 @@ export default {
await this.$apollo.mutate({
mutation: this.deleteMutation,
variables: {
- id: fullBoardId(this.board.id),
+ id: this.board.id,
},
});
},
@@ -285,19 +263,12 @@ export default {
}
},
setIteration(iterationId) {
- this.board.iteration_id = iterationId;
+ this.$set(this.board, 'iteration', {
+ id: iterationId,
+ });
},
setBoardLabels(labels) {
- labels.forEach((label) => {
- if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
- this.board.labels.push({
- ...label,
- textColor: label.text_color,
- });
- } else if (!label.set) {
- this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
- }
- });
+ this.board.labels = labels;
},
setAssignee(assigneeId) {
this.$set(this.board, 'assignee', {
@@ -361,8 +332,8 @@ export default {
</div>
<board-configuration-options
- :hide-backlog-list.sync="board.hide_backlog_list"
- :hide-closed-list.sync="board.hide_closed_list"
+ :hide-backlog-list.sync="board.hideBacklogList"
+ :hide-closed-list.sync="board.hideClosedList"
:readonly="readonly"
/>
@@ -371,11 +342,6 @@ export default {
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :enable-scoped-labels="enableScopedLabels"
- :project-id="projectId"
- :group-id="groupId"
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a8d71ab7a35..e985a368e64 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -15,6 +15,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
@@ -40,7 +42,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin()],
+ mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
boardId: {
default: '',
@@ -86,6 +88,13 @@ export default {
listTitle() {
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
+ listIterationPeriod() {
+ const iteration = this.list?.iteration;
+ return iteration ? this.getIterationPeriod(iteration) : '';
+ },
+ isIterationList() {
+ return this.listType === ListType.iteration;
+ },
showListHeaderButton() {
return !this.disabled && this.listType !== ListType.closed;
},
@@ -96,7 +105,10 @@ export default {
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
- return this.listType === ListType.iteration && this.showListDetails;
+ return this.isIterationList && this.showListDetails;
+ },
+ iterationCadencesAvailable() {
+ return this.isIterationList && this.glFeatures.iterationCadences;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
@@ -208,6 +220,16 @@ export default {
updateListFunction() {
this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
},
+ /**
+ * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
+ * This method also exists as a utility function in ee/../iterations/utils.js
+ * Remove the duplication when the EE code is separated from this compoment.
+ */
+ getIterationPeriod({ startDate, dueDate }) {
+ const start = formatDate(startDate, 'mmm d, yyyy', true);
+ const due = formatDate(dueDate, 'mmm d, yyyy', true);
+ return `${start} - ${due}`;
+ },
},
};
</script>
@@ -307,6 +329,13 @@ export default {
class="board-title-main-text gl-text-truncate"
>
{{ listTitle }}
+ <span
+ v-if="iterationCadencesAvailable"
+ class="gl-display-inline-block gl-text-gray-400"
+ data-testid="board-list-iteration-period"
+ >
+ {{ listIterationPeriod }}</span
+ >
</span>
<span
v-if="listType === 'assignee'"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 98027917221..71facba1378 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -9,17 +9,20 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import { throttle } from 'lodash';
-import { mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { s__ } from '~/locale';
import eventHub from '../eventhub';
-import groupQuery from '../graphql/group_boards.query.graphql';
-import projectQuery from '../graphql/project_boards.query.graphql';
+import groupBoardsQuery from '../graphql/group_boards.query.graphql';
+import projectBoardsQuery from '../graphql/project_boards.query.graphql';
+import groupBoardQuery from '../graphql/group_board.query.graphql';
+import projectBoardQuery from '../graphql/project_board.query.graphql';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -39,10 +42,6 @@ export default {
},
inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
- currentBoard: {
- type: Object,
- required: true,
- },
throttleDuration: {
type: Number,
default: 200,
@@ -64,22 +63,6 @@ export default {
type: Boolean,
required: true,
},
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- groupId: {
- type: Number,
- required: true,
- },
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
@@ -88,11 +71,6 @@ export default {
type: Array,
required: true,
},
- enabledScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -107,14 +85,47 @@ export default {
maxPosition: 0,
filterTerm: '',
currentPage: '',
+ board: {},
};
},
+ apollo: {
+ board: {
+ query() {
+ return this.currentBoardQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ boardId: this.fullBoardId,
+ };
+ },
+ update(data) {
+ const board = data.workspace?.board;
+ return {
+ ...board,
+ labels: board?.labels?.nodes,
+ };
+ },
+ error() {
+ this.setError({ message: this.$options.i18n.errorFetchingBoard });
+ },
+ },
+ },
computed: {
- ...mapState(['boardType']),
- ...mapGetters(['isGroupBoard']),
+ ...mapState(['boardType', 'fullBoardId']),
+ ...mapGetters(['isGroupBoard', 'isProjectBoard']),
parentType() {
return this.boardType;
},
+ currentBoardQueryCE() {
+ return this.isGroupBoard ? groupBoardQuery : projectBoardQuery;
+ },
+ currentBoardQuery() {
+ return this.currentBoardQueryCE;
+ },
+ isBoardLoading() {
+ return this.$apollo.queries.board.loading;
+ },
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
@@ -123,9 +134,6 @@ export default {
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
- board() {
- return this.currentBoard;
- },
showCreate() {
return this.multipleIssueBoardsAvailable;
},
@@ -158,6 +166,7 @@ export default {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
+ ...mapActions(['setError']),
showPage(page) {
this.currentPage = page;
},
@@ -174,7 +183,7 @@ export default {
}));
},
boardQuery() {
- return this.isGroupBoard ? groupQuery : projectQuery;
+ return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
@@ -250,6 +259,9 @@ export default {
this.hasScrollFade = this.isScrolledUp();
},
},
+ i18n: {
+ errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'),
+ },
};
</script>
@@ -260,6 +272,7 @@ export default {
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
+ :loading="isBoardLoading"
:text="board.name"
@show="loadBoards"
>
@@ -354,15 +367,10 @@ export default {
<board-form
v-if="currentPage"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :project-id="projectId"
- :group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
- :enable-scoped-labels="enabledScopedLabels"
- :current-board="currentBoard"
+ :current-board="board"
:current-page="currentPage"
@cancel="cancel"
/>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index b6c5ef955c6..bdb9c2be836 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,13 +1,20 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapActions } from 'vuex';
-import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
+import { BoardType } from '~/boards/constants';
+import axios from '~/lib/utils/axios_utils';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ DEFAULT_MILESTONES_GRAPHQL,
+ TOKEN_TITLE_MY_REACTION,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
@@ -19,6 +26,7 @@ export default {
},
i18n: {
search: __('Search'),
+ epic: __('Epic'),
label: __('Label'),
author: __('Author'),
assignee: __('Assignee'),
@@ -31,6 +39,7 @@ export default {
isNot: __('is not'),
},
components: { BoardFilteredSearch },
+ inject: ['isSignedIn'],
props: {
fullPath: {
type: String,
@@ -42,7 +51,15 @@ export default {
},
},
computed: {
- tokens() {
+ isGroupBoard() {
+ return this.boardType === BoardType.group;
+ },
+ epicsGroupPath() {
+ return this.isGroupBoard
+ ? this.fullPath
+ : this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
+ },
+ tokensCE() {
const {
label,
is,
@@ -103,6 +120,32 @@ export default {
symbol: '~',
fetchLabels,
},
+ ...(this.isSignedIn
+ ? [
+ {
+ type: 'my_reaction_emoji',
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: (search = '') => {
+ // TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/339694
+ return axios
+ .get(`${gon.relative_url_root || ''}/-/autocomplete/award_emojis`)
+ .then(({ data }) => {
+ if (search) {
+ return {
+ data: fuzzaldrinPlus.filter(data, search, {
+ key: ['name'],
+ }),
+ };
+ }
+ return { data };
+ });
+ },
+ },
+ ]
+ : []),
{
type: 'milestone_title',
title: milestone,
@@ -117,7 +160,6 @@ export default {
icon: 'issues',
title: type,
type: 'types',
- operators: [{ value: '=', description: is }],
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -134,6 +176,9 @@ export default {
},
];
},
+ tokens() {
+ return this.tokensCE;
+ },
},
methods: {
...mapActions(['fetchMilestones']),
diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue
new file mode 100644
index 00000000000..f7914c636cc
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_board_button.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { formType } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
+
+export default {
+ components: {
+ GlButton,
+ GitlabExperiment,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['multipleIssueBoardsAvailable', 'canAdminBoard'],
+ computed: {
+ canShowCreateButton() {
+ return this.canAdminBoard && this.multipleIssueBoardsAvailable;
+ },
+ createButtonText() {
+ return s__('Boards|New board');
+ },
+ },
+ methods: {
+ showDialog() {
+ this.track('click_button', { label: 'create_board' });
+ eventHub.$emit('showBoardModal', formType.new);
+ },
+ },
+};
+</script>
+
+<template>
+ <gitlab-experiment name="prominent_create_board_btn">
+ <template #control> </template>
+ <template #candidate>
+ <div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
+ <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
+ {{ createButtonText }}
+ </gl-button>
+ </div>
+ </template>
+ </gitlab-experiment>
+</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index e74463825c5..ec53947fd5f 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -91,9 +91,7 @@ export default {
try {
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
- const removeLabelIds = this.selectedLabels
- .filter((label) => !payload.find((selected) => selected.id === label.id))
- .map((label) => label.id);
+ const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
const input = {
addLabelIds,
@@ -164,7 +162,7 @@ export default {
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
:is-editing="edit"
- variant="embedded"
+ variant="sidebar"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
>
diff --git a/app/assets/javascripts/boards/graphql.js b/app/assets/javascripts/boards/graphql.js
index d8d16184936..64938cb42ed 100644
--- a/app/assets/javascripts/boards/graphql.js
+++ b/app/assets/javascripts/boards/graphql.js
@@ -17,6 +17,5 @@ export const gqlClient = createDefaultClient(
fragmentMatcher,
},
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
index 3eb23f62940..0e1d11727cf 100644
--- a/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_list_create.mutation.graphql
@@ -1,6 +1,6 @@
#import "./board_list.fragment.graphql"
-mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
+mutation createBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
list {
...BoardListFragment
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index 734867c77e9..47e87907d76 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/board_list.fragment.graphql"
-query ListIssues(
+query BoardLists(
$fullPath: ID!
$boardId: ID!
$filters: BoardIssueInput
diff --git a/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
new file mode 100644
index 00000000000..57f51822d91
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql
@@ -0,0 +1,6 @@
+fragment BoardScopeFragment on Board {
+ id
+ name
+ hideBacklogList
+ hideClosedList
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board.query.graphql b/app/assets/javascripts/boards/graphql/group_board.query.graphql
new file mode 100644
index 00000000000..77c8e0378f0
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board.query.graphql
@@ -0,0 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
+query GroupBoard($fullPath: ID!, $boardId: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ board(id: $boardId) {
+ ...BoardScopeFragment
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
deleted file mode 100644
index 1c382c4747b..00000000000
--- a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query GroupBoardIterations($fullPath: ID!, $title: String) {
- group(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
index 3218c06357c..c5732bbaff3 100644
--- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getGroupProjects($fullPath: ID!, $search: String, $after: String) {
+query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
group(fullPath: $fullPath) {
projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
nodes {
diff --git a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
index 3c574fd8c87..570731ecac6 100644
--- a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
-mutation IssueMoveList(
+mutation issueMoveList(
$projectPath: ID!
$iid: String!
$boardId: ID!
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 787dd77b901..9f93bc6d5bf 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -1,6 +1,6 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
-query ListIssues(
+query BoardListEE(
$fullPath: ID!
$boardId: ID!
$id: ID
diff --git a/app/assets/javascripts/boards/graphql/project_board.query.graphql b/app/assets/javascripts/boards/graphql/project_board.query.graphql
new file mode 100644
index 00000000000..6e4cd6bed57
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board.query.graphql
@@ -0,0 +1,9 @@
+#import "ee_else_ce/boards/graphql/board_scope.fragment.graphql"
+
+query ProjectBoard($fullPath: ID!, $boardId: ID!) {
+ workspace: project(fullPath: $fullPath) {
+ board(id: $boardId) {
+ ...BoardScopeFragment
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
deleted file mode 100644
index 078151a275a..00000000000
--- a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query ProjectBoardIterations($fullPath: ID!, $title: String) {
- project(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index 724b7f5a34c..61c9ddded9b 100644
--- a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,4 +1,4 @@
-query projectMilestones(
+query boardProjectMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
$includeAncestors: Boolean
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index b6b1094fb3a..6fa8dd63245 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -13,9 +13,10 @@ import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import toggleFocusMode from '~/boards/toggle_focus';
-import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
+import { NavigationType, isLoggedIn, parseBoolean } from '~/lib/utils/common_utils';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
+import initNewBoard from './new_board';
import { gqlClient } from './graphql';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
@@ -109,7 +110,7 @@ export default () => {
});
if (gon?.features?.issueBoardsFilteredSearch) {
- initBoardsFilteredSearch(apolloProvider);
+ initBoardsFilteredSearch(apolloProvider, isLoggedIn());
}
mountBoardApp($boardApp);
@@ -130,6 +131,7 @@ export default () => {
}
boardConfigToggle();
+ initNewBoard();
toggleFocusMode();
toggleLabels();
@@ -142,5 +144,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
+ allowScopedLabels: $boardApp.dataset.scopedLabels,
+ labelsManagePath: $boardApp.dataset.labelsManagePath,
});
};
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
index 7732091ef34..1ea74d5685c 100644
--- a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
+import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
-export default (apolloProvider) => {
+export default (apolloProvider, isSignedIn) => {
const el = document.getElementById('js-issue-board-filtered-search');
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@@ -20,6 +20,7 @@ export default (apolloProvider) => {
el,
provide: {
initialFilterParams,
+ isSignedIn,
},
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider,
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index a3a8ad06c43..ed32579a9c3 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,23 +1,32 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
+import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
Vue.use(VueApollo);
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
- assumeImmutableResults: true,
+ cacheConfig: {
+ fragmentMatcher,
+ },
},
),
});
export default (params = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
+ const { dataset } = boardsSwitcherElement;
return new Vue({
el: boardsSwitcherElement,
components: {
@@ -29,18 +38,16 @@ export default (params = {}) => {
fullPath: params.fullPath,
rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint,
+ allowScopedLabels: params.allowScopedLabels,
+ labelsManagePath: params.labelsManagePath,
+ allowLabelCreate: parseBoolean(dataset.canAdminBoard),
},
data() {
- const { dataset } = boardsSwitcherElement;
-
const boardsSelectorProps = {
...dataset,
- currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
- projectId: dataset.projectId ? Number(dataset.projectId) : 0,
- groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
};
diff --git a/app/assets/javascripts/boards/new_board.js b/app/assets/javascripts/boards/new_board.js
new file mode 100644
index 00000000000..34f2fea79a9
--- /dev/null
+++ b/app/assets/javascripts/boards/new_board.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getExperimentVariant } from '~/experimentation/utils';
+import { CANDIDATE_VARIANT } from '~/experimentation/constants';
+import NewBoardButton from './components/new_board_button.vue';
+
+export default () => {
+ if (getExperimentVariant('prominent_create_board_btn') !== CANDIDATE_VARIANT) {
+ return;
+ }
+
+ const el = document.querySelector('.js-new-board');
+
+ if (!el) {
+ return;
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ provide: {
+ multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleIssueBoardsAvailable),
+ canAdminBoard: parseBoolean(el.dataset.canAdminBoard),
+ },
+ render(h) {
+ return h(NewBoardButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index ca993e75cf9..3a96e535cf7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -36,13 +36,11 @@ import {
} from '../boards_util';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
-import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types';
@@ -203,52 +201,6 @@ export default {
});
},
- fetchIterations({ state, commit }, title) {
- commit(types.RECEIVE_ITERATIONS_REQUEST);
-
- const { fullPath, boardType } = state;
-
- const variables = {
- fullPath,
- title,
- };
-
- let query;
- if (boardType === BoardType.project) {
- query = projectBoardIterationsQuery;
- }
- if (boardType === BoardType.group) {
- query = groupBoardIterationsQuery;
- }
-
- if (!query) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Unknown board type');
- }
-
- return gqlClient
- .query({
- query,
- variables,
- })
- .then(({ data }) => {
- const errors = data[boardType]?.errors;
- const iterations = data[boardType]?.iterations.nodes;
-
- if (errors?.[0]) {
- throw new Error(errors[0]);
- }
-
- commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
-
- return iterations;
- })
- .catch((e) => {
- commit(types.RECEIVE_ITERATIONS_FAILURE);
- throw e;
- });
- },
-
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
@@ -656,30 +608,45 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetLabelsMutation,
- variables: {
- input: {
- iid: input.iid || String(activeBoardItem.iid),
- addLabelIds: input.addLabelIds ?? [],
- removeLabelIds: input.removeLabelIds ?? [],
- projectPath: input.projectPath,
+
+ if (!gon.features?.labelsWidget) {
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetLabelsMutation,
+ variables: {
+ input: {
+ iid: input.iid || String(activeBoardItem.iid),
+ labelIds: input.labelsId ?? undefined,
+ addLabelIds: input.addLabelIds ?? [],
+ removeLabelIds: input.removeLabelIds ?? [],
+ projectPath: input.projectPath,
+ },
},
- },
- });
+ });
- commit(types.SET_LABELS_LOADING, false);
+ if (data.updateIssue?.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ prop: 'labels',
+ value: data.updateIssue?.issue?.labels.nodes,
+ });
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
+ return;
}
+ let labels = input?.labels || [];
+ if (input.removeLabelIds) {
+ labels = activeBoardItem.labels.filter(
+ (label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
+ );
+ }
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ itemId: input.id || activeBoardItem.id,
prop: 'labels',
- value: data.updateIssue.issue.labels.nodes,
+ value: labels,
});
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 26b785932bb..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
-export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
@@ -42,7 +41,3 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
export const SET_ERROR = 'SET_ERROR';
-
-export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
-export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
-export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index d381c076c19..2a2ce7652e6 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -64,20 +64,6 @@ export default {
);
},
- [mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
- state.iterationsLoading = true;
- },
-
- [mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
- state.iterations = iterations;
- state.iterationsLoading = false;
- },
-
- [mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
- state.iterationsLoading = false;
- state.error = __('Failed to load iterations.');
- },
-
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
state.sidebarType = sidebarType;
@@ -195,10 +181,6 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
- [mutationTypes.SET_LABELS_LOADING](state, isLoading) {
- state.isSettingLabels = isLoading;
- },
-
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 2a6605e687b..80c51c966d2 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -12,7 +12,6 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
- isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index b8b8a0b2867..b9d3742974c 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import { hide } from '~/tooltips';
export const addTooltipToEl = (el) => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
@@ -19,16 +18,23 @@ export default () => {
.filter((el) => !el.classList.contains('dropdown'))
.map((el) => el.querySelector('a'))
.filter((el) => el);
- const $expander = $('.js-breadcrumbs-collapsed-expander');
+ const $expanderBtn = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach((el) => addTooltipToEl(el));
- $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', (e) => {
- const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget);
+ $expanderBtn.on('click', () => {
+ const detailItems = $('.breadcrumbs-detail-item');
+ const hiddenClass = 'gl-display-none!';
- $el.toggleClass('open');
+ $.each(detailItems, (_key, item) => {
+ $(item).toggleClass(hiddenClass);
+ });
- hide($el);
+ // remove the ellipsis
+ $('li.expander').remove();
+
+ // set focus on first breadcrumb item
+ $('.breadcrumb-item-text').first().focus();
});
}
};
diff --git a/app/assets/javascripts/chronic_duration.js b/app/assets/javascripts/chronic_duration.js
new file mode 100644
index 00000000000..1073d736b06
--- /dev/null
+++ b/app/assets/javascripts/chronic_duration.js
@@ -0,0 +1,417 @@
+/*
+ * NOTE:
+ * Changes to this file should be kept in sync with
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration/-/blob/master/lib/gitlab_chronic_duration.rb.
+ */
+
+/*
+ * This code is based on code from
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration and is
+ * distributed under the following license:
+ *
+ * MIT License
+ *
+ * Copyright (c) Henry Poydar
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+export class DurationParseError extends Error {}
+
+// On average, there's a little over 4 weeks in month.
+const FULL_WEEKS_PER_MONTH = 4;
+
+const HOURS_PER_DAY = 24;
+const DAYS_PER_MONTH = 30;
+
+const FLOAT_MATCHER = /[0-9]*\.?[0-9]+/g;
+const DURATION_UNITS_LIST = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'];
+
+const MAPPINGS = {
+ seconds: 'seconds',
+ second: 'seconds',
+ secs: 'seconds',
+ sec: 'seconds',
+ s: 'seconds',
+ minutes: 'minutes',
+ minute: 'minutes',
+ mins: 'minutes',
+ min: 'minutes',
+ m: 'minutes',
+ hours: 'hours',
+ hour: 'hours',
+ hrs: 'hours',
+ hr: 'hours',
+ h: 'hours',
+ days: 'days',
+ day: 'days',
+ dy: 'days',
+ d: 'days',
+ weeks: 'weeks',
+ week: 'weeks',
+ wks: 'weeks',
+ wk: 'weeks',
+ w: 'weeks',
+ months: 'months',
+ mo: 'months',
+ mos: 'months',
+ month: 'months',
+ years: 'years',
+ year: 'years',
+ yrs: 'years',
+ yr: 'years',
+ y: 'years',
+};
+
+const JOIN_WORDS = ['and', 'with', 'plus'];
+
+function convertToNumber(string) {
+ const f = parseFloat(string);
+ return f % 1 > 0 ? f : parseInt(string, 10);
+}
+
+function durationUnitsSecondsMultiplier(unit, opts) {
+ if (!DURATION_UNITS_LIST.includes(unit)) {
+ return 0;
+ }
+
+ const hoursPerDay = opts.hoursPerDay || HOURS_PER_DAY;
+ const daysPerMonth = opts.daysPerMonth || DAYS_PER_MONTH;
+ const daysPerWeek = Math.trunc(daysPerMonth / FULL_WEEKS_PER_MONTH);
+
+ switch (unit) {
+ case 'years':
+ return 31557600;
+ case 'months':
+ return 3600 * hoursPerDay * daysPerMonth;
+ case 'weeks':
+ return 3600 * hoursPerDay * daysPerWeek;
+ case 'days':
+ return 3600 * hoursPerDay;
+ case 'hours':
+ return 3600;
+ case 'minutes':
+ return 60;
+ case 'seconds':
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+function calculateFromWords(string, opts) {
+ let val = 0;
+ const words = string.split(' ');
+ words.forEach((v, k) => {
+ if (v === '') {
+ return;
+ }
+ if (v.search(FLOAT_MATCHER) >= 0) {
+ val +=
+ convertToNumber(v) *
+ durationUnitsSecondsMultiplier(
+ words[parseInt(k, 10) + 1] || opts.defaultUnit || 'seconds',
+ opts,
+ );
+ }
+ });
+ return val;
+}
+
+// Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
+function filterByType(string) {
+ const chronoUnitsList = DURATION_UNITS_LIST.filter((v) => v !== 'weeks');
+ if (
+ string
+ .replace(/ +/g, '')
+ .search(RegExp(`${FLOAT_MATCHER.source}(:${FLOAT_MATCHER.source})+`, 'g')) >= 0
+ ) {
+ const res = [];
+ string
+ .replace(/ +/g, '')
+ .split(':')
+ .reverse()
+ .forEach((v, k) => {
+ if (!chronoUnitsList[k]) {
+ return;
+ }
+ res.push(`${v} ${chronoUnitsList[k]}`);
+ });
+ return res.reverse().join(' ');
+ }
+ return string;
+}
+
+// Get rid of unknown words and map found
+// words to defined time units
+function filterThroughWhiteList(string, opts) {
+ const res = [];
+ string.split(' ').forEach((word) => {
+ if (word === '') {
+ return;
+ }
+ if (word.search(FLOAT_MATCHER) >= 0) {
+ res.push(word.trim());
+ return;
+ }
+ const strippedWord = word.trim().replace(/^,/g, '').replace(/,$/g, '');
+ if (MAPPINGS[strippedWord] !== undefined) {
+ res.push(MAPPINGS[strippedWord]);
+ } else if (!JOIN_WORDS.includes(strippedWord) && opts.raiseExceptions) {
+ throw new DurationParseError(
+ `An invalid word ${JSON.stringify(word)} was used in the string to be parsed.`,
+ );
+ }
+ });
+ // add '1' at front if string starts with something recognizable but not with a number, like 'day' or 'minute 30sec'
+ if (res.length > 0 && MAPPINGS[res[0]]) {
+ res.splice(0, 0, 1);
+ }
+ return res.join(' ');
+}
+
+function cleanup(string, opts) {
+ let res = string.toLowerCase();
+ /*
+ * TODO The Ruby implementation of this algorithm uses the Numerizer module,
+ * which converts strings like "forty two" to "42", but there is no
+ * JavaScript equivalent of Numerizer. Skip it for now until Numerizer is
+ * ported to JavaScript.
+ */
+ res = filterByType(res);
+ res = res
+ .replace(FLOAT_MATCHER, (n) => ` ${n} `)
+ .replace(/ +/g, ' ')
+ .trim();
+ return filterThroughWhiteList(res, opts);
+}
+
+function humanizeTimeUnit(number, unit, pluralize, keepZero) {
+ if (number === '0' && !keepZero) {
+ return null;
+ }
+ let res = number + unit;
+ // A poor man's pluralizer
+ if (number !== '1' && pluralize) {
+ res += 's';
+ }
+ return res;
+}
+
+// Given a string representation of elapsed time,
+// return an integer (or float, if fractions of a
+// second are input)
+export function parseChronicDuration(string, opts = {}) {
+ const result = calculateFromWords(cleanup(string, opts), opts);
+ return !opts.keepZero && result === 0 ? null : result;
+}
+
+// Given an integer and an optional format,
+// returns a formatted string representing elapsed time
+export function outputChronicDuration(seconds, opts = {}) {
+ const units = {
+ years: 0,
+ months: 0,
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds,
+ };
+
+ const hoursPerDay = opts.hoursPerDay || HOURS_PER_DAY;
+ const daysPerMonth = opts.daysPerMonth || DAYS_PER_MONTH;
+ const daysPerWeek = Math.trunc(daysPerMonth / FULL_WEEKS_PER_MONTH);
+
+ const decimalPlaces =
+ seconds % 1 !== 0 ? seconds.toString().split('.').reverse()[0].length : null;
+
+ const minute = 60;
+ const hour = 60 * minute;
+ const day = hoursPerDay * hour;
+ const month = daysPerMonth * day;
+ const year = 31557600;
+
+ if (units.seconds >= 31557600 && units.seconds % year < units.seconds % month) {
+ units.years = Math.trunc(units.seconds / year);
+ units.months = Math.trunc((units.seconds % year) / month);
+ units.days = Math.trunc(((units.seconds % year) % month) / day);
+ units.hours = Math.trunc((((units.seconds % year) % month) % day) / hour);
+ units.minutes = Math.trunc(((((units.seconds % year) % month) % day) % hour) / minute);
+ units.seconds = Math.trunc(((((units.seconds % year) % month) % day) % hour) % minute);
+ } else if (seconds >= 60) {
+ units.minutes = Math.trunc(seconds / 60);
+ units.seconds %= 60;
+ if (units.minutes >= 60) {
+ units.hours = Math.trunc(units.minutes / 60);
+ units.minutes = Math.trunc(units.minutes % 60);
+ if (!opts.limitToHours) {
+ if (units.hours >= hoursPerDay) {
+ units.days = Math.trunc(units.hours / hoursPerDay);
+ units.hours = Math.trunc(units.hours % hoursPerDay);
+ if (opts.weeks) {
+ if (units.days >= daysPerWeek) {
+ units.weeks = Math.trunc(units.days / daysPerWeek);
+ units.days = Math.trunc(units.days % daysPerWeek);
+ if (units.weeks >= FULL_WEEKS_PER_MONTH) {
+ units.months = Math.trunc(units.weeks / FULL_WEEKS_PER_MONTH);
+ units.weeks = Math.trunc(units.weeks % FULL_WEEKS_PER_MONTH);
+ }
+ }
+ } else if (units.days >= daysPerMonth) {
+ units.months = Math.trunc(units.days / daysPerMonth);
+ units.days = Math.trunc(units.days % daysPerMonth);
+ }
+ }
+ }
+ }
+ }
+
+ let joiner = opts.joiner || ' ';
+ let process = null;
+
+ let dividers;
+ switch (opts.format) {
+ case 'micro':
+ dividers = {
+ years: 'y',
+ months: 'mo',
+ weeks: 'w',
+ days: 'd',
+ hours: 'h',
+ minutes: 'm',
+ seconds: 's',
+ };
+ joiner = '';
+ break;
+ case 'short':
+ dividers = {
+ years: 'y',
+ months: 'mo',
+ weeks: 'w',
+ days: 'd',
+ hours: 'h',
+ minutes: 'm',
+ seconds: 's',
+ };
+ break;
+ case 'long':
+ dividers = {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ years: ' year',
+ months: ' month',
+ weeks: ' week',
+ days: ' day',
+ hours: ' hour',
+ minutes: ' minute',
+ seconds: ' second',
+ /* eslint-enable @gitlab/require-i18n-strings */
+ pluralize: true,
+ };
+ break;
+ case 'chrono':
+ dividers = {
+ years: ':',
+ months: ':',
+ weeks: ':',
+ days: ':',
+ hours: ':',
+ minutes: ':',
+ seconds: ':',
+ keepZero: true,
+ };
+ process = (str) => {
+ // Pad zeros
+ // Get rid of lead off times if they are zero
+ // Get rid of lead off zero
+ // Get rid of trailing:
+ const divider = ':';
+ const processed = [];
+ str.split(divider).forEach((n) => {
+ if (n === '') {
+ return;
+ }
+ // add zeros only if n is an integer
+ if (n.search('\\.') >= 0) {
+ processed.push(
+ parseFloat(n)
+ .toFixed(decimalPlaces)
+ .padStart(3 + decimalPlaces, '0'),
+ );
+ } else {
+ processed.push(n.padStart(2, '0'));
+ }
+ });
+ return processed
+ .join(divider)
+ .replace(/^(00:)+/g, '')
+ .replace(/^0/g, '')
+ .replace(/:$/g, '');
+ };
+ joiner = '';
+ break;
+ default:
+ dividers = {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ years: ' yr',
+ months: ' mo',
+ weeks: ' wk',
+ days: ' day',
+ hours: ' hr',
+ minutes: ' min',
+ seconds: ' sec',
+ /* eslint-enable @gitlab/require-i18n-strings */
+ pluralize: true,
+ };
+ break;
+ }
+
+ let result = [];
+ ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'].forEach((t) => {
+ if (t === 'weeks' && !opts.weeks) {
+ return;
+ }
+ let num = units[t];
+ if (t === 'seconds' && num % 0 !== 0) {
+ num = num.toFixed(decimalPlaces);
+ } else {
+ num = num.toString();
+ }
+ const keepZero = !dividers.keepZero && t === 'seconds' ? opts.keepZero : dividers.keepZero;
+ const humanized = humanizeTimeUnit(num, dividers[t], dividers.pluralize, keepZero);
+ if (humanized !== null) {
+ result.push(humanized);
+ }
+ });
+
+ if (opts.units) {
+ result = result.slice(0, opts.units);
+ }
+
+ result = result.join(joiner);
+
+ if (process) {
+ result = process(result);
+ }
+
+ return result.length === 0 ? null : result;
+}
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js
index f97590ec5db..274aab45deb 100644
--- a/app/assets/javascripts/ci_lint/index.js
+++ b/app/assets/javascripts/ci_lint/index.js
@@ -8,9 +8,7 @@ import CiLint from './components/ci_lint.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, {
- assumeImmutableResults: true,
- }),
+ defaultClient: createDefaultClient(resolvers),
});
export default (containerId = '#js-ci-lint') => {
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 5c672d288c5..afbba9d1f7c 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -128,6 +128,7 @@ export default {
</p>
<gl-tabs>
+ <slot name="ee-security-tab"></slot>
<gl-tab>
<template #title>
<span data-testid="cluster-agent-token-count">
diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js
index bcb5b271203..426d8d83847 100644
--- a/app/assets/javascripts/clusters/agents/index.js
+++ b/app/assets/javascripts/clusters/agents/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import AgentShowPage from './components/show.vue';
+import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 0da7be4040f..98db620e3ab 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,8 +1,7 @@
<script>
-import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
-import { escape } from 'lodash';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
-import { s__, sprintf } from '~/locale';
+import { s__ } from '~/locale';
import SplitButton from '~/vue_shared/components/split_button.vue';
const splitButtonActionItems = [
@@ -29,6 +28,7 @@ export default {
GlModal,
GlButton,
GlFormInput,
+ GlSprintf,
},
props: {
clusterPath: {
@@ -67,17 +67,11 @@ export default {
: s__('ClusterIntegration|You are about to remove your cluster integration.');
},
confirmationTextLabel() {
- return sprintf(
- this.confirmCleanup
- ? s__(
- 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
- )
- : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
- {
- clusterName: `<code>${escape(this.clusterName)}</code>`,
- },
- false,
- );
+ return this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
+ )
+ : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:');
},
canSubmit() {
return this.enteredClusterName === this.clusterName;
@@ -140,7 +134,13 @@ export default {
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</ul>
</div>
- <strong v-html="confirmationTextLabel /* eslint-disable-line vue/no-v-html */"></strong>
+ <strong>
+ <gl-sprintf :message="confirmationTextLabel">
+ <template #clusterName>
+ <code>{{ clusterName }}</code>
+ </template>
+ </gl-sprintf>
+ </strong>
<form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
@@ -159,7 +159,7 @@ export default {
)
}}</span>
<template #modal-footer>
- <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
+ <gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
<gl-button
:disabled="!canSubmit"
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index 9b870134512..c78c93fe1ba 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -6,3 +6,7 @@ export function generateAgentRegistrationCommand(agentToken, kasAddress) {
--agent-version stable \\
--namespace gitlab-kubernetes-agent | kubectl apply -f -`;
}
+
+export function getAgentConfigPath(clusterAgentName) {
+ return `.gitlab/agents/${clusterAgentName}`;
+}
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index 405339b3d36..af44a23b4b3 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -1,9 +1,16 @@
<script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
-import { INSTALL_AGENT_MODAL_ID } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default {
+ i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID,
+ multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ installDocsUrl: helpPagePath('administration/clusters/kas'),
+ getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
+ anchor: 'define-a-configuration-repository',
+ }),
components: {
GlButton,
GlEmptyState,
@@ -14,19 +21,17 @@ export default {
directives: {
GlModalDirective,
},
- inject: [
- 'emptyStateImage',
- 'projectPath',
- 'agentDocsUrl',
- 'installDocsUrl',
- 'getStartedDocsUrl',
- 'integrationDocsUrl',
- ],
+ inject: ['emptyStateImage', 'projectPath'],
props: {
hasConfigurations: {
type: Boolean,
required: true,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
},
computed: {
repositoryPath() {
@@ -37,22 +42,19 @@ export default {
</script>
<template>
- <gl-empty-state
- :svg-path="emptyStateImage"
- :title="s__('ClusterAgents|Integrate Kubernetes with a GitLab Agent')"
- class="empty-state--agent"
- >
+ <gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
- <p class="mw-460 gl-mx-auto">
- <gl-sprintf
- :message="
- s__(
- 'ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}',
- )
- "
- >
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ {{ $options.i18n.introText }}
+ </p>
+ <p class="mw-460 gl-mx-auto gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
- <gl-link :href="agentDocsUrl" target="_blank" data-testid="agent-docs-link">
+ <gl-link
+ :href="$options.multipleClustersDocsUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
{{ content }}
</gl-link>
</template>
@@ -60,19 +62,9 @@ export default {
</p>
<p class="mw-460 gl-mx-auto">
- <gl-sprintf
- :message="
- s__(
- 'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
+ <gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
+ {{ $options.i18n.learnMoreText }}
+ </gl-link>
</p>
<gl-alert
@@ -81,24 +73,20 @@ export default {
class="gl-mb-5 text-left"
:dismissible="false"
>
- {{
- s__(
- 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
- )
- }}
+ {{ $options.i18n.warningText }}
<template #actions>
<gl-button
category="primary"
variant="info"
- :href="getStartedDocsUrl"
+ :href="$options.getStartedDocsUrl"
target="_blank"
class="gl-ml-0!"
>
- {{ s__('ClusterAgents|Read more about getting started') }}
+ {{ $options.i18n.readMoreText }}
</gl-button>
<gl-button category="secondary" variant="info" :href="repositoryPath">
- {{ s__('ClusterAgents|Go to the repository') }}
+ {{ $options.i18n.repositoryButtonText }}
</gl-button>
</template>
</gl-alert>
@@ -106,13 +94,14 @@ export default {
<template #actions>
<gl-button
+ v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button"
category="primary"
- variant="success"
+ variant="confirm"
>
- {{ s__('ClusterAgents|Integrate with the GitLab Agent') }}
+ {{ $options.i18n.primaryButtonText }}
</gl-button>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 487e512c06d..000730ac1ba 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -1,6 +1,5 @@
<script>
import {
- GlButton,
GlLink,
GlModalDirective,
GlTable,
@@ -12,11 +11,12 @@ import {
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES, TROUBLESHOOTING_LINK } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES } from '../constants';
+import { getAgentConfigPath } from '../clusters_util';
export default {
components: {
- GlButton,
GlLink,
GlTable,
GlIcon,
@@ -29,10 +29,12 @@ export default {
GlModalDirective,
},
mixins: [timeagoMixin],
- inject: ['integrationDocsUrl'],
INSTALL_AGENT_MODAL_ID,
AGENT_STATUSES,
- TROUBLESHOOTING_LINK,
+
+ troubleshooting_link: helpPagePath('user/clusters/agent/index', {
+ anchor: 'troubleshooting',
+ }),
props: {
agents: {
required: true,
@@ -41,112 +43,102 @@ export default {
},
computed: {
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: s__('ClusterAgents|Name'),
+ tdClass,
},
{
key: 'status',
label: s__('ClusterAgents|Connection status'),
+ tdClass,
},
{
key: 'lastContact',
label: s__('ClusterAgents|Last contact'),
+ tdClass,
},
{
key: 'configuration',
label: s__('ClusterAgents|Configuration'),
+ tdClass,
},
];
},
},
+ methods: {
+ getCellId(item) {
+ return `connection-status-${item.name}`;
+ },
+ getAgentConfigPath,
+ },
};
</script>
<template>
- <div>
- <div class="gl-display-block gl-text-right gl-my-3">
- <gl-button
- v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
- variant="confirm"
- category="primary"
- >{{ s__('ClusterAgents|Install a new GitLab Agent') }}
- </gl-button>
- </div>
+ <gl-table
+ :items="agents"
+ :fields="fields"
+ stacked="md"
+ head-variant="white"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ class="gl-mb-4!"
+ data-testid="cluster-agent-list-table"
+ >
+ <template #cell(name)="{ item }">
+ <gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
+ {{ item.name }}
+ </gl-link>
+ </template>
- <gl-table
- :items="agents"
- :fields="fields"
- stacked="md"
- head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- data-testid="cluster-agent-list-table"
- >
- <template #cell(name)="{ item }">
- <gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
- {{ item.name }}
- </gl-link>
- </template>
-
- <template #cell(status)="{ item }">
- <span
- :id="`connection-status-${item.name}`"
- class="gl-pr-5"
- data-testid="cluster-agent-connection-status"
- >
- <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
- <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
- >{{ $options.AGENT_STATUSES[item.status].name }}
- </span>
- <gl-tooltip
- v-if="item.status === 'active'"
- :target="`connection-status-${item.name}`"
- placement="right"
- >
- <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
- ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
- </gl-sprintf>
- </gl-tooltip>
- <gl-popover
- v-else
- :target="`connection-status-${item.name}`"
- :title="$options.AGENT_STATUSES[item.status].tooltip.title"
- placement="right"
- container="viewport"
- >
- <p>
- <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
- ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
- >
- </p>
- <p class="gl-mb-0">
- {{ s__('ClusterAgents|For more troubleshooting information go to') }}
- <gl-link :href="$options.TROUBLESHOOTING_LINK" target="_blank" class="gl-font-sm">
- {{ $options.TROUBLESHOOTING_LINK }}</gl-link
- >
- </p>
- </gl-popover>
- </template>
+ <template #cell(status)="{ item }">
+ <span :id="getCellId(item)" class="gl-md-pr-5" data-testid="cluster-agent-connection-status">
+ <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
+ <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
+ >{{ $options.AGENT_STATUSES[item.status].name }}
+ </span>
+ <gl-tooltip v-if="item.status === 'active'" :target="getCellId(item)" placement="right">
+ <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
+ ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
+ </gl-sprintf>
+ </gl-tooltip>
+ <gl-popover
+ v-else
+ :target="getCellId(item)"
+ :title="$options.AGENT_STATUSES[item.status].tooltip.title"
+ placement="right"
+ container="viewport"
+ >
+ <p>
+ <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
+ ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
+ >
+ </p>
+ <p class="gl-mb-0">
+ <gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm">
+ {{ s__('ClusterAgents|Learn how to troubleshoot') }}</gl-link
+ >
+ </p>
+ </gl-popover>
+ </template>
- <template #cell(lastContact)="{ item }">
- <span data-testid="cluster-agent-last-contact">
- <time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
- <span v-else>{{ s__('ClusterAgents|Never') }}</span>
- </span>
- </template>
+ <template #cell(lastContact)="{ item }">
+ <span data-testid="cluster-agent-last-contact">
+ <time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
+ <span v-else>{{ s__('ClusterAgents|Never') }}</span>
+ </span>
+ </template>
- <template #cell(configuration)="{ item }">
- <span data-testid="cluster-agent-configuration-link">
- <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
- .gitlab/agents/{{ item.name }}
- </gl-link>
+ <template #cell(configuration)="{ item }">
+ <span data-testid="cluster-agent-configuration-link">
+ <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
+ {{ getAgentConfigPath(item.name) }}
+ </gl-link>
- <span v-else>.gitlab/agents/{{ item.name }}</span>
- <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
- </span>
- </template>
- </gl-table>
- </div>
+ <span v-else>{{ getAgentConfigPath(item.name) }}</span>
+ </span>
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index ed44c1f5fa7..fb5cf7d1206 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -4,7 +4,6 @@ import { MAX_LIST_COUNT, ACTIVE_CONNECTION_TIME } from '../constants';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AgentEmptyState from './agent_empty_state.vue';
import AgentTable from './agent_table.vue';
-import InstallAgentModal from './install_agent_modal.vue';
export default {
apollo: {
@@ -21,12 +20,14 @@ export default {
this.updateTreeList(data);
return data;
},
+ result() {
+ this.emitAgentsLoaded();
+ },
},
},
components: {
AgentEmptyState,
AgentTable,
- InstallAgentModal,
GlAlert,
GlKeysetPagination,
GlLoadingIcon,
@@ -38,11 +39,21 @@ export default {
required: false,
type: String,
},
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
},
data() {
return {
cursor: {
- first: MAX_LIST_COUNT,
+ first: this.limit ? this.limit : MAX_LIST_COUNT,
last: null,
},
folderList: {},
@@ -70,7 +81,7 @@ export default {
return this.$apollo.queries.agents.loading;
},
showPagination() {
- return this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage;
+ return !this.limit && (this.agentPageInfo.hasPreviousPage || this.agentPageInfo.hasNextPage);
},
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
@@ -130,24 +141,31 @@ export default {
}
return 'unused';
},
+ emitAgentsLoaded() {
+ const count = this.agents?.project?.clusterAgents?.count;
+ this.$emit('onAgentsLoad', count);
+ },
},
};
</script>
<template>
- <gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="isLoading" size="md" />
- <section v-else-if="agentList" class="gl-mt-3">
+ <section v-else-if="agentList">
<div v-if="agentList.length">
- <AgentTable :agents="agentList" />
+ <agent-table :agents="agentList" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" />
</div>
</div>
- <AgentEmptyState v-else :has-configurations="hasConfigurations" />
- <InstallAgentModal @agentRegistered="reloadAgents" />
+ <agent-empty-state
+ v-else
+ :has-configurations="hasConfigurations"
+ :is-child-component="isChildComponent"
+ />
</section>
<gl-alert v-else variant="danger" :dismissible="false">
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 0d1534d20e0..9c330045596 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -14,6 +14,7 @@ import { __, sprintf } from '~/locale';
import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
+import ClustersEmptyState from './clusters_empty_state.vue';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
@@ -28,10 +29,23 @@ export default {
GlSprintf,
GlTable,
NodeErrorHelpText,
+ ClustersEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ limit: {
+ default: null,
+ required: false,
+ type: Number,
+ },
+ },
computed: {
...mapState([
'clusters',
@@ -40,7 +54,7 @@ export default {
'loadingNodes',
'page',
'providers',
- 'totalCulsters',
+ 'totalClusters',
]),
contentAlignClasses() {
return 'gl-display-flex gl-align-items-center gl-justify-content-end gl-justify-content-md-start';
@@ -55,43 +69,57 @@ export default {
},
},
fields() {
+ const tdClass = 'gl-py-5!';
return [
{
key: 'name',
label: __('Kubernetes cluster'),
+ tdClass,
},
{
key: 'environment_scope',
label: __('Environment scope'),
+ tdClass,
},
{
key: 'node_size',
label: __('Nodes'),
+ tdClass,
},
{
key: 'total_cpu',
label: __('Total cores (CPUs)'),
+ tdClass,
},
{
key: 'total_memory',
label: __('Total memory (GB)'),
+ tdClass,
},
{
key: 'cluster_type',
label: __('Cluster level'),
+ tdClass,
formatter: (value) => CLUSTER_TYPES[value],
},
];
},
- hasClusters() {
+ hasClustersPerPage() {
return this.clustersPerPage > 0;
},
+ hasClusters() {
+ return this.totalClusters > 0;
+ },
},
mounted() {
+ if (this.limit) {
+ this.setClustersPerPage(this.limit);
+ }
+
this.fetchClusters();
},
methods: {
- ...mapActions(['fetchClusters', 'reportSentryError', 'setPage']),
+ ...mapActions(['fetchClusters', 'reportSentryError', 'setPage', 'setClustersPerPage']),
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
@@ -196,18 +224,20 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="loadingClusters" size="md" class="gl-mt-3" />
+ <gl-loading-icon v-if="loadingClusters" size="md" />
<section v-else>
<ancestor-notice />
<gl-table
+ v-if="hasClusters"
:items="clusters"
:fields="fields"
+ fixed
stacked="md"
head-variant="white"
- thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- class="qa-clusters-table"
+ thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
+ class="qa-clusters-table gl-mb-4!"
data-testid="cluster_list_table"
>
<template #cell(name)="{ item }">
@@ -241,7 +271,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.connection_error"
@@ -262,7 +292,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.node_connection_error"
@@ -283,7 +313,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <NodeErrorHelpText
+ <node-error-help-text
v-else-if="item.kubernetes_errors"
:class="contentAlignClasses"
:error-type="item.kubernetes_errors.metrics_connection_error"
@@ -298,11 +328,13 @@ export default {
</template>
</gl-table>
+ <clusters-empty-state v-else :is-child-component="isChildComponent" />
+
<gl-pagination
- v-if="hasClusters"
+ v-if="hasClustersPerPage && !limit"
v-model="currentPage"
:per-page="clustersPerPage"
- :total-items="totalCulsters"
+ :total-items="totalClusters"
:prev-text="__('Prev')"
:next-text="__('Next')"
align="center"
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
new file mode 100644
index 00000000000..25f67462223
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
+import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
+
+export default {
+ i18n: CLUSTERS_ACTIONS,
+ INSTALL_AGENT_MODAL_ID,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['newClusterPath', 'addClusterPath'],
+};
+</script>
+
+<template>
+ <div class="nav-controls gl-ml-auto">
+ <gl-dropdown
+ ref="dropdown"
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ category="primary"
+ variant="confirm"
+ :text="$options.i18n.actionsButton"
+ split
+ right
+ >
+ <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
+ {{ $options.i18n.createNewCluster }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ data-testid="connect-new-agent-link"
+ >
+ {{ $options.i18n.connectWithAgent }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
+ {{ $options.i18n.connectExistingCluster }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
new file mode 100644
index 00000000000..3879af6e9cb
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { I18N_CLUSTERS_EMPTY_STATE } from '../constants';
+
+export default {
+ i18n: I18N_CLUSTERS_EMPTY_STATE,
+ components: {
+ GlEmptyState,
+ GlButton,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
+ props: {
+ isChildComponent: {
+ default: false,
+ required: false,
+ type: Boolean,
+ },
+ },
+ learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
+ multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
+ computed: {
+ ...mapState(['canAddCluster']),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state :svg-path="clustersEmptyStateImage" title="">
+ <template #description>
+ <p class="gl-text-left">
+ {{ $options.i18n.description }}
+ </p>
+ <p class="gl-text-left">
+ <gl-sprintf :message="$options.i18n.multipleClustersText">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.multipleClustersHelpUrl"
+ target="_blank"
+ data-testid="multiple-clusters-docs-link"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
+ {{ emptyStateHelpText }}
+ </p>
+
+ <p>
+ <gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
+ {{ $options.i18n.learnMoreLinkText }}
+ </gl-link>
+ </p>
+ </template>
+
+ <template #actions>
+ <gl-button
+ v-if="!isChildComponent"
+ data-testid="integration-primary-button"
+ data-qa-selector="add_kubernetes_cluster_link"
+ category="primary"
+ variant="confirm"
+ :disabled="!canAddCluster"
+ :href="newClusterPath"
+ >
+ {{ $options.i18n.buttonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_main_view.vue b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
new file mode 100644
index 00000000000..9e03093aa67
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_main_view.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { CLUSTERS_TABS, MAX_CLUSTERS_LIST, MAX_LIST_COUNT, AGENT } from '../constants';
+import Agents from './agents.vue';
+import InstallAgentModal from './install_agent_modal.vue';
+import ClustersActions from './clusters_actions.vue';
+import Clusters from './clusters.vue';
+import ClustersViewAll from './clusters_view_all.vue';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ ClustersActions,
+ ClustersViewAll,
+ Clusters,
+ Agents,
+ InstallAgentModal,
+ },
+ CLUSTERS_TABS,
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ selectedTabIndex: 0,
+ maxAgents: MAX_CLUSTERS_LIST,
+ };
+ },
+ methods: {
+ onTabChange(tabName) {
+ this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
+
+ this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tabs
+ v-model="selectedTabIndex"
+ sync-active-tab-with-query-params
+ nav-class="gl-flex-grow-1 gl-align-items-center"
+ lazy
+ >
+ <gl-tab
+ v-for="(tab, idx) in $options.CLUSTERS_TABS"
+ :key="idx"
+ :title="tab.title"
+ :query-param-value="tab.queryParamValue"
+ class="gl-line-height-20 gl-mt-5"
+ >
+ <component
+ :is="tab.component"
+ :default-branch-name="defaultBranchName"
+ data-testid="clusters-tab-component"
+ @changeTab="onTabChange"
+ />
+ </gl-tab>
+
+ <template #tabs-end>
+ <clusters-actions />
+ </template>
+ </gl-tabs>
+
+ <install-agent-modal :default-branch-name="defaultBranchName" :max-agents="maxAgents" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_view_all.vue b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
new file mode 100644
index 00000000000..285876e57d8
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/clusters_view_all.vue
@@ -0,0 +1,218 @@
+<script>
+import {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { mapState } from 'vuex';
+import {
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+} from '../constants';
+import Clusters from './clusters.vue';
+import Agents from './agents.vue';
+
+export default {
+ components: {
+ GlCard,
+ GlSprintf,
+ GlPopover,
+ GlLink,
+ GlButton,
+ GlBadge,
+ GlLoadingIcon,
+ Clusters,
+ Agents,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+ inject: ['addClusterPath'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ loadingAgents: true,
+ totalAgents: null,
+ };
+ },
+ computed: {
+ ...mapState(['loadingClusters', 'totalClusters']),
+ isLoading() {
+ return this.loadingAgents || this.loadingClusters;
+ },
+ agentsCardTitle() {
+ let cardTitle;
+ if (this.totalAgents > 0) {
+ cardTitle = {
+ message: AGENT_CARD_INFO.title,
+ number: this.totalAgents < MAX_CLUSTERS_LIST ? this.totalAgents : MAX_CLUSTERS_LIST,
+ total: this.totalAgents,
+ };
+ } else {
+ cardTitle = {
+ message: AGENT_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ clustersCardTitle() {
+ let cardTitle;
+ if (this.totalClusters > 0) {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.title,
+ number: this.totalClusters < MAX_CLUSTERS_LIST ? this.totalClusters : MAX_CLUSTERS_LIST,
+ total: this.totalClusters,
+ };
+ } else {
+ cardTitle = {
+ message: CERTIFICATE_BASED_CARD_INFO.emptyTitle,
+ };
+ }
+
+ return cardTitle;
+ },
+ },
+ methods: {
+ cardFooterNumber(number) {
+ return number > MAX_CLUSTERS_LIST ? number : '';
+ },
+ onAgentsLoad(number) {
+ this.totalAgents = number;
+ this.loadingAgents = false;
+ },
+ changeTab($event, tab) {
+ $event.preventDefault();
+ this.$emit('changeTab', tab);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <div v-show="!isLoading" data-testid="clusters-cards-container">
+ <gl-card
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between gl-py-4"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3 data-testid="agent-card-title" class="gl-my-0 gl-font-weight-normal gl-font-size-h2">
+ <gl-sprintf :message="agentsCardTitle.message"
+ ><template #number>{{ agentsCardTitle.number }}</template>
+ <template #total>{{ agentsCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+
+ <gl-badge id="clusters-recommended-badge" size="md" variant="info">{{
+ $options.AGENT_CARD_INFO.tooltip.label
+ }}</gl-badge>
+
+ <gl-popover
+ target="clusters-recommended-badge"
+ container="viewport"
+ placement="bottom"
+ :title="$options.AGENT_CARD_INFO.tooltip.title"
+ >
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.AGENT_CARD_INFO.tooltip.text">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.AGENT_CARD_INFO.tooltip.link"
+ target="_blank"
+ class="gl-font-sm"
+ >
+ {{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-popover>
+ </template>
+
+ <agents
+ :limit="$options.MAX_CLUSTERS_LIST"
+ :default-branch-name="defaultBranchName"
+ :is-child-component="true"
+ @onAgentsLoad="onAgentsLoad"
+ />
+
+ <template #footer>
+ <gl-link
+ v-if="totalAgents"
+ data-testid="agents-tab-footer-link"
+ :href="`?tab=${$options.AGENT_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.AGENT_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.AGENT_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
+ class="gl-ml-4"
+ category="secondary"
+ variant="confirm"
+ >{{ $options.AGENT_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+
+ <gl-card
+ class="gl-mt-6"
+ header-class="gl-bg-white gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ body-class="gl-pb-0"
+ footer-class="gl-text-right"
+ >
+ <template #header>
+ <h3
+ class="gl-my-1 gl-font-weight-normal gl-font-size-h2"
+ data-testid="clusters-card-title"
+ >
+ <gl-sprintf :message="clustersCardTitle.message"
+ ><template #number>{{ clustersCardTitle.number }}</template>
+ <template #total>{{ clustersCardTitle.total }}</template>
+ </gl-sprintf>
+ </h3>
+ </template>
+
+ <clusters :limit="$options.MAX_CLUSTERS_LIST" :is-child-component="true" />
+
+ <template #footer>
+ <gl-link
+ v-if="totalClusters"
+ data-testid="clusters-tab-footer-link"
+ :href="`?tab=${$options.CERTIFICATE_BASED_CARD_INFO.tabName}`"
+ @click="changeTab($event, $options.CERTIFICATE_BASED_CARD_INFO.tabName)"
+ ><gl-sprintf :message="$options.CERTIFICATE_BASED_CARD_INFO.footerText"
+ ><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
+ ></gl-link
+ ><gl-button
+ category="secondary"
+ data-qa-selector="connect_existing_cluster_button"
+ variant="confirm"
+ class="gl-ml-4"
+ :href="addClusterPath"
+ >{{ $options.CERTIFICATE_BASED_CARD_INFO.actionText }}</gl-button
+ >
+ </template>
+ </gl-card>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
index 5f192fe4d5a..6eb2e85ecea 100644
--- a/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
+++ b/app/assets/javascripts/clusters_list/components/install_agent_modal.vue
@@ -13,8 +13,10 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants';
+import { addAgentToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
+import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default {
@@ -33,12 +35,24 @@ export default {
GlSprintf,
},
inject: ['projectPath', 'kasAddress'],
+ props: {
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ maxAgents: {
+ required: true,
+ type: Number,
+ },
+ },
data() {
return {
registering: false,
agentName: null,
agentToken: null,
error: null,
+ clusterAgent: null,
};
},
computed: {
@@ -55,27 +69,31 @@ export default {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
basicInstallPath() {
- return helpPagePath('user/clusters/agent/index', {
+ return helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
});
},
advancedInstallPath() {
- return helpPagePath('user/clusters/agent/index', { anchor: 'advanced-installation' });
+ return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
+ },
+ getAgentsQueryVariables() {
+ return {
+ defaultBranchName: this.defaultBranchName,
+ first: this.maxAgents,
+ last: null,
+ projectPath: this.projectPath,
+ };
},
},
methods: {
setAgentName(name) {
this.agentName = name;
},
- cancelClicked() {
- this.$refs.modal.hide();
- },
- doneClicked() {
- this.$emit('agentRegistered');
+ closeModal() {
this.$refs.modal.hide();
},
resetModal() {
- this.registering = null;
+ this.registering = false;
this.agentName = null;
this.agentToken = null;
this.error = null;
@@ -90,6 +108,14 @@ export default {
projectPath: this.projectPath,
},
},
+ update: (store, { data: { createClusterAgent } }) => {
+ addAgentToStore(
+ store,
+ createClusterAgent,
+ getAgentsQuery,
+ this.getAgentsQueryVariables,
+ );
+ },
})
.then(({ data: { createClusterAgent } }) => createClusterAgent);
},
@@ -117,6 +143,8 @@ export default {
throw new Error(agentErrors[0]);
}
+ this.clusterAgent = clusterAgent;
+
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation(
clusterAgent.id,
);
@@ -240,10 +268,10 @@ export default {
</template>
<template #modal-footer>
- <gl-button v-if="canCancel" @click="cancelClicked">{{ $options.i18n.cancel }} </gl-button>
+ <gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
- <gl-button v-if="registered" variant="confirm" category="primary" @click="doneClicked"
- >{{ $options.i18n.done }}
+ <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
+ >{{ $options.i18n.close }}
</gl-button>
<gl-button
@@ -252,7 +280,7 @@ export default {
variant="confirm"
category="primary"
@click="registerAgent"
- >{{ $options.i18n.next }}
+ >{{ $options.i18n.registerAgentButton }}
</gl-button>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 0bade1fc281..9fefdf450c4 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -1,10 +1,9 @@
import { __, s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
-export const TROUBLESHOOTING_LINK =
- 'https://docs.gitlab.com/ee/user/clusters/agent/#troubleshooting';
export const CLUSTER_ERRORS = {
default: {
@@ -66,8 +65,8 @@ export const STATUSES = {
};
export const I18N_INSTALL_AGENT_MODAL = {
- next: __('Next'),
- done: __('Done'),
+ registerAgentButton: s__('ClusterAgents|Register Agent'),
+ close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'),
@@ -91,7 +90,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
- basicInstallBody: s__(
+ basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
@@ -100,7 +99,7 @@ export const I18N_INSTALL_AGENT_MODAL = {
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
- registrationErrorTitle: s__('Failed to register Agent'),
+ registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
@@ -141,3 +140,86 @@ export const AGENT_STATUSES = {
},
},
};
+
+export const I18N_AGENTS_EMPTY_STATE = {
+ introText: s__(
+ 'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
+ warningText: s__(
+ 'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
+ ),
+ readMoreText: s__('ClusterAgents|Read more about getting started'),
+ repositoryButtonText: s__('ClusterAgents|Go to the repository'),
+ primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
+};
+
+export const I18N_CLUSTERS_EMPTY_STATE = {
+ description: s__(
+ 'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.',
+ ),
+ multipleClustersText: s__(
+ 'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
+ ),
+ learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
+ buttonText: s__('ClusterIntegration|Connect with a certificate'),
+};
+
+export const AGENT_CARD_INFO = {
+ tabName: 'agent',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Agent based integrations')),
+ emptyTitle: s__('ClusterAgents|No Agent based integrations'),
+ tooltip: {
+ label: s__('ClusterAgents|Recommended'),
+ title: s__('ClusterAgents|GitLab Agents'),
+ text: sprintf(
+ s__(
+ 'ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}',
+ ),
+ ),
+ link: helpPagePath('user/clusters/agent/index'),
+ },
+ actionText: s__('ClusterAgents|Install new Agent'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Agent based integrations')),
+};
+
+export const CERTIFICATE_BASED_CARD_INFO = {
+ tabName: 'certificate_based',
+ title: sprintf(s__('ClusterAgents|%{number} of %{total} Certificate based integrations')),
+ emptyTitle: s__('ClusterAgents|No Certificate based integrations'),
+ actionText: s__('ClusterAgents|Connect existing cluster'),
+ footerText: sprintf(s__('ClusterAgents|View all %{number} Certificate based integrations')),
+};
+
+export const MAX_CLUSTERS_LIST = 6;
+
+export const CLUSTERS_TABS = [
+ {
+ title: s__('ClusterAgents|All'),
+ component: 'ClustersViewAll',
+ queryParamValue: 'all',
+ },
+ {
+ title: s__('ClusterAgents|Agent'),
+ component: 'agents',
+ queryParamValue: 'agent',
+ },
+ {
+ title: s__('ClusterAgents|Certificate based'),
+ component: 'clusters',
+ queryParamValue: 'certificate_based',
+ },
+];
+
+export const CLUSTERS_ACTIONS = {
+ actionsButton: s__('ClusterAgents|Actions'),
+ createNewCluster: s__('ClusterAgents|Create new cluster'),
+ connectWithAgent: s__('ClusterAgents|Connect with Agent'),
+ connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
+};
+
+export const AGENT = 'agent';
+export const CERTIFICATE_BASED = 'certificate_based';
diff --git a/app/assets/javascripts/clusters_list/graphql/cache_update.js b/app/assets/javascripts/clusters_list/graphql/cache_update.js
new file mode 100644
index 00000000000..dd633820952
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/cache_update.js
@@ -0,0 +1,29 @@
+import produce from 'immer';
+import { getAgentConfigPath } from '../clusters_util';
+
+export function addAgentToStore(store, createClusterAgent, query, variables) {
+ const { clusterAgent } = createClusterAgent;
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ const configuration = {
+ name: clusterAgent.name,
+ path: getAgentConfigPath(clusterAgent.name),
+ webPath: clusterAgent.webPath,
+ __typename: 'TreeEntry',
+ };
+
+ draftData.project.clusterAgents.nodes.push(clusterAgent);
+ draftData.project.clusterAgents.count += 1;
+ draftData.project.repository.tree.trees.nodes.push(configuration);
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
new file mode 100644
index 00000000000..9b40260471c
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql
@@ -0,0 +1,10 @@
+fragment ClusterAgentFragment on ClusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ lastUsedAt
+ }
+ }
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
index c29756159f5..996b388089b 100644
--- a/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/mutations/create_agent.mutation.graphql
@@ -1,7 +1,9 @@
+#import "../fragments/cluster_agent.fragment.graphql"
+
mutation createClusterAgent($input: CreateClusterAgentInput!) {
createClusterAgent(input: $input) {
clusterAgent {
- id
+ ...ClusterAgentFragment
}
errors
}
diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
index 61989e00d9e..47b25988877 100644
--- a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
+++ b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql
@@ -1,4 +1,5 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/cluster_agent.fragment.graphql"
query getAgents(
$defaultBranchName: String!
@@ -13,19 +14,14 @@ query getAgents(
project(fullPath: $projectPath) {
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
nodes {
- id
- name
- webPath
- tokens {
- nodes {
- lastUsedAt
- }
- }
+ ...ClusterAgentFragment
}
pageInfo {
...PageInfo
}
+
+ count
}
repository {
diff --git a/app/assets/javascripts/clusters_list/index.js b/app/assets/javascripts/clusters_list/index.js
index de18965abbd..7f1ef37814b 100644
--- a/app/assets/javascripts/clusters_list/index.js
+++ b/app/assets/javascripts/clusters_list/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
-import loadAgents from './load_agents';
+import loadMainView from './load_main_view';
Vue.use(VueApollo);
export default () => {
loadClusters(Vue);
- loadAgents(Vue, VueApollo);
+ loadMainView(Vue, VueApollo);
};
diff --git a/app/assets/javascripts/clusters_list/load_agents.js b/app/assets/javascripts/clusters_list/load_agents.js
deleted file mode 100644
index b77d386df20..00000000000
--- a/app/assets/javascripts/clusters_list/load_agents.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import createDefaultClient from '~/lib/graphql';
-import Agents from './components/agents.vue';
-
-export default (Vue, VueApollo) => {
- const el = document.querySelector('#js-cluster-agents-list');
-
- if (!el) {
- return null;
- }
-
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
-
- const {
- emptyStateImage,
- defaultBranchName,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- } = el.dataset;
-
- return new Vue({
- el,
- apolloProvider: new VueApollo({ defaultClient }),
- provide: {
- emptyStateImage,
- projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
- kasAddress,
- },
- render(createElement) {
- return createElement(Agents, {
- props: {
- defaultBranchName,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/clusters_list/load_clusters.js b/app/assets/javascripts/clusters_list/load_clusters.js
index 01430230879..1bb3ea546b2 100644
--- a/app/assets/javascripts/clusters_list/load_clusters.js
+++ b/app/assets/javascripts/clusters_list/load_clusters.js
@@ -8,8 +8,15 @@ export default (Vue) => {
return null;
}
+ const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
+
return new Vue({
el,
+ provide: {
+ emptyStateHelpText,
+ newClusterPath,
+ clustersEmptyStateImage,
+ },
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
diff --git a/app/assets/javascripts/clusters_list/load_main_view.js b/app/assets/javascripts/clusters_list/load_main_view.js
new file mode 100644
index 00000000000..08c99b46e16
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/load_main_view.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import ClustersMainView from './components/clusters_main_view.vue';
+import { createStore } from './store';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.querySelector('.js-clusters-main-view');
+
+ if (!el) {
+ return null;
+ }
+
+ const defaultClient = createDefaultClient();
+
+ const {
+ emptyStateImage,
+ defaultBranchName,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider: new VueApollo({ defaultClient }),
+ provide: {
+ emptyStateImage,
+ projectPath,
+ kasAddress,
+ newClusterPath,
+ addClusterPath,
+ emptyStateHelpText,
+ clustersEmptyStateImage,
+ },
+ store: createStore(el.dataset),
+ render(createElement) {
+ return createElement(ClustersMainView, {
+ props: {
+ defaultBranchName,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 5f35a0b26f3..d70b36e63bc 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -3,7 +3,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import { MAX_REQUESTS } from '../constants';
import * as types from './mutation_types';
@@ -30,7 +30,13 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
const poll = new Poll({
resource: {
- fetchClusters: (paginatedEndPoint) => axios.get(paginatedEndPoint),
+ fetchClusters: (paginatedEndPoint) =>
+ axios.get(paginatedEndPoint, {
+ params: {
+ page: state.page,
+ per_page: state.clustersPerPage,
+ },
+ }),
},
data: `${state.endpoint}?page=${state.page}`,
method: 'fetchClusters',
@@ -65,7 +71,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
createFlash({
- message: __('Clusters|An error occurred while loading clusters'),
+ message: s__('Clusters|An error occurred while loading clusters'),
});
dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' });
@@ -78,3 +84,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
+
+export const setClustersPerPage = ({ commit }, limit) => {
+ commit(types.SET_CLUSTERS_PER_PAGE, limit);
+};
diff --git a/app/assets/javascripts/clusters_list/store/mutation_types.js b/app/assets/javascripts/clusters_list/store/mutation_types.js
index beb4388c93e..e88d4c74761 100644
--- a/app/assets/javascripts/clusters_list/store/mutation_types.js
+++ b/app/assets/javascripts/clusters_list/store/mutation_types.js
@@ -2,3 +2,4 @@ export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_CLUSTERS = 'SET_LOADING_CLUSTERS';
export const SET_LOADING_NODES = 'SET_LOADING_NODES';
export const SET_PAGE = 'SET_PAGE';
+export const SET_CLUSTERS_PER_PAGE = 'SET_CLUSTERS_PER_PAGE';
diff --git a/app/assets/javascripts/clusters_list/store/mutations.js b/app/assets/javascripts/clusters_list/store/mutations.js
index 5b462928518..93156c9200f 100644
--- a/app/assets/javascripts/clusters_list/store/mutations.js
+++ b/app/assets/javascripts/clusters_list/store/mutations.js
@@ -12,10 +12,13 @@ export default {
clusters: data.clusters,
clustersPerPage: paginationInformation.perPage,
hasAncestorClusters: data.has_ancestor_clusters,
- totalCulsters: paginationInformation.total,
+ totalClusters: paginationInformation.total,
});
},
[types.SET_PAGE](state, value) {
state.page = Number(value) || 1;
},
+ [types.SET_CLUSTERS_PER_PAGE](state, value) {
+ state.clustersPerPage = Number(value) || 1;
+ },
};
diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js
index 51fafd49479..763d7389d0f 100644
--- a/app/assets/javascripts/clusters_list/store/state.js
+++ b/app/assets/javascripts/clusters_list/store/state.js
@@ -1,9 +1,11 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
export default (initialState = {}) => ({
ancestorHelperPath: initialState.ancestorHelpPath,
endpoint: initialState.endpoint,
hasAncestorClusters: false,
clusters: [],
- clustersPerPage: 0,
+ clustersPerPage: 20,
loadingClusters: true,
loadingNodes: true,
page: 1,
@@ -12,5 +14,6 @@ export default (initialState = {}) => ({
default: { path: initialState.imgTagsDefaultPath, text: initialState.imgTagsDefaultText },
gcp: { path: initialState.imgTagsGcpPath, text: initialState.imgTagsGcpText },
},
- totalCulsters: 0,
+ totalClusters: 0,
+ canAddCluster: parseBoolean(initialState.canAddCluster),
});
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 5b9e70e3c09..ad70d9be16f 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -3,14 +3,14 @@ import { Rails } from '~/lib/utils/rails_ujs';
import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, $modal, text) {
- const $input = $('.js-confirm-danger-input', $modal);
+ const $input = $('.js-legacy-confirm-danger-input', $modal);
$input.val('');
$('.js-confirm-text', $modal).text(text || '');
$modal.modal('show');
- const confirmTextMatch = $('.js-confirm-danger-match', $modal).text();
- const $submit = $('.js-confirm-danger-submit', $modal);
+ const confirmTextMatch = $('.js-legacy-confirm-danger-match', $modal).text();
+ const $submit = $('.js-legacy-confirm-danger-submit', $modal);
$submit.disable();
$input.focus();
@@ -25,7 +25,7 @@ function openConfirmDangerModal($form, $modal, text) {
});
// eslint-disable-next-line @gitlab/no-global-event-off
- $('.js-confirm-danger-submit', $modal)
+ $('.js-legacy-confirm-danger-submit', $modal)
.off('click')
.on('click', () => {
if ($form.data('remote')) {
@@ -47,7 +47,7 @@ function getModal($btn) {
}
export default function initConfirmDangerModal() {
- $(document).on('click', '.js-confirm-danger', (e) => {
+ $(document).on('click', '.js-legacy-confirm-danger', (e) => {
const $btn = $(e.target);
const checkFieldName = $btn.data('checkFieldName');
const checkFieldCompareValue = $btn.data('checkCompareValue');
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 02ab34447ca..a8405fe37c7 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -3,7 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor';
-import ContentEditorError from './content_editor_error.vue';
+import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
@@ -12,7 +12,7 @@ import TopToolbar from './top_toolbar.vue';
export default {
components: {
GlLoadingIcon,
- ContentEditorError,
+ ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
@@ -92,7 +92,7 @@ export default {
<content-editor-provider :content-editor="contentEditor">
<div>
<editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
- <content-editor-error />
+ <content-editor-alert />
<div
data-testid="content-editor"
data-qa-selector="content_editor_container"
diff --git a/app/assets/javascripts/content_editor/components/content_editor_alert.vue b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
new file mode 100644
index 00000000000..c6737da1d77
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_alert.vue
@@ -0,0 +1,33 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import EditorStateObserver from './editor_state_observer.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EditorStateObserver,
+ },
+ data() {
+ return {
+ message: null,
+ variant: 'danger',
+ };
+ },
+ methods: {
+ displayAlert({ message, variant }) {
+ this.message = message;
+ this.variant = variant;
+ },
+ dismissAlert() {
+ this.message = null;
+ },
+ },
+};
+</script>
+<template>
+ <editor-state-observer @alert="displayAlert">
+ <gl-alert v-if="message" class="gl-mb-6" :variant="variant" @dismiss="dismissAlert">
+ {{ message }}
+ </gl-alert>
+ </editor-state-observer>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_error.vue b/app/assets/javascripts/content_editor/components/content_editor_error.vue
deleted file mode 100644
index 031ea92a7e9..00000000000
--- a/app/assets/javascripts/content_editor/components/content_editor_error.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<script>
-import { GlAlert } from '@gitlab/ui';
-import EditorStateObserver from './editor_state_observer.vue';
-
-export default {
- components: {
- GlAlert,
- EditorStateObserver,
- },
- data() {
- return {
- error: null,
- };
- },
- methods: {
- displayError({ error }) {
- this.error = error;
- },
- dismissError() {
- this.error = null;
- },
- },
-};
-</script>
-<template>
- <editor-state-observer @error="displayError">
- <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
- {{ error }}
- </gl-alert>
- </editor-state-observer>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 2eeb0719096..0604047a953 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -7,7 +7,7 @@ export const tiptapToComponentMap = {
transaction: 'transaction',
focus: 'focus',
blur: 'blur',
- error: 'error',
+ alert: 'alert',
};
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index c44e8145982..41c083111c5 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -26,8 +26,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
@@ -61,7 +61,17 @@ export default {
const { state } = this.editor;
const { $cursor } = state.selection;
- this.displayActionsDropdown = $cursor?.pos - $cursor?.parentOffset - 1 === this.getPos();
+ if (!$cursor) return;
+
+ this.displayActionsDropdown = false;
+
+ for (let level = 0; level < $cursor.depth; level += 1) {
+ if ($cursor.node(level) === this.node) {
+ this.displayActionsDropdown = true;
+ break;
+ }
+ }
+
if (this.displayActionsDropdown) {
this.selectedRect = getSelectedRect(state);
}
@@ -99,7 +109,11 @@ export default {
:as="cellType"
@click="hideDropdown"
>
- <span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
+ <span
+ v-if="displayActionsDropdown"
+ contenteditable="false"
+ class="gl-absolute gl-right-0 gl-top-0"
+ >
<gl-dropdown
ref="dropdown"
dropup
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
index 6b4343dd5b8..47cd837d060 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
@@ -11,8 +11,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
index 5f9889374f6..150f78bc84f 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
@@ -11,8 +11,8 @@ export default {
type: Object,
required: true,
},
- getPos: {
- type: Function,
+ node: {
+ type: Object,
required: true,
},
},
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
index 4512ead44bc..5632bc28592 100644
--- a/app/assets/javascripts/content_editor/extensions/blockquote.js
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -1,10 +1,8 @@
import { Blockquote } from '@tiptap/extension-blockquote';
-import { wrappingInputRule } from 'prosemirror-inputrules';
+import { wrappingInputRule } from '@tiptap/core';
import { getParents } from '~/lib/utils/dom_utils';
import { getMarkdownSource } from '../services/markdown_sourcemap';
-export const multilineInputRegex = /^\s*>>>\s$/gm;
-
export default Blockquote.extend({
addAttributes() {
return {
@@ -25,9 +23,15 @@ export default Blockquote.extend({
},
addInputRules() {
+ const multilineInputRegex = /^\s*>>>\s$/gm;
+
return [
...this.parent?.(),
- wrappingInputRule(multilineInputRegex, this.type, () => ({ multiline: true })),
+ wrappingInputRule({
+ find: multilineInputRegex,
+ type: this.type,
+ getAttributes: () => ({ multiline: true }),
+ }),
];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/description_list.js b/app/assets/javascripts/content_editor/extensions/description_list.js
index a516dfad2b8..8f5b145cfa3 100644
--- a/app/assets/javascripts/content_editor/extensions/description_list.js
+++ b/app/assets/javascripts/content_editor/extensions/description_list.js
@@ -1,7 +1,4 @@
-import { Node, mergeAttributes } from '@tiptap/core';
-import { wrappingInputRule } from 'prosemirror-inputrules';
-
-export const inputRegex = /^\s*(<dl>)$/;
+import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
export default Node.create({
name: 'descriptionList',
@@ -18,6 +15,8 @@ export default Node.create({
},
addInputRules() {
- return [wrappingInputRule(inputRegex, this.type)];
+ const inputRegex = /^\s*(<dl>)$/;
+
+ return [wrappingInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/details.js b/app/assets/javascripts/content_editor/extensions/details.js
index e3d54ed01fd..46c906d45b1 100644
--- a/app/assets/javascripts/content_editor/extensions/details.js
+++ b/app/assets/javascripts/content_editor/extensions/details.js
@@ -1,10 +1,7 @@
-import { Node } from '@tiptap/core';
+import { Node, wrappingInputRule } from '@tiptap/core';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import { wrappingInputRule } from 'prosemirror-inputrules';
import DetailsWrapper from '../components/wrappers/details.vue';
-export const inputRegex = /^\s*(<details>)$/;
-
export default Node.create({
name: 'details',
content: 'detailsContent+',
@@ -24,7 +21,9 @@ export default Node.create({
},
addInputRules() {
- return [wrappingInputRule(inputRegex, this.type)];
+ const inputRegex = /^\s*(<details>)$/;
+
+ return [wrappingInputRule({ find: inputRegex, type: this.type })];
},
addCommands() {
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index de608c3aaa2..7f8b5da5f46 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,9 +1,6 @@
-import { Node } from '@tiptap/core';
-import { InputRule } from 'prosemirror-inputrules';
+import { Node, InputRule } from '@tiptap/core';
import { initEmojiMap, getAllEmoji } from '~/emoji';
-export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/;
-
export default Node.create({
name: 'emoji',
@@ -54,23 +51,28 @@ export default Node.create({
},
addInputRules() {
+ const emojiInputRegex = /(?:^|\s)(:(\w+):)$/;
+
return [
- new InputRule(emojiInputRegex, (state, match, start, end) => {
- const [, , name] = match;
- const emojis = getAllEmoji();
- const emoji = emojis[name];
- const { tr } = state;
+ new InputRule({
+ find: emojiInputRegex,
+ handler: ({ state, range: { from, to }, match }) => {
+ const [, , name] = match;
+ const emojis = getAllEmoji();
+ const emoji = emojis[name];
+ const { tr } = state;
- if (emoji) {
- tr.replaceWith(start, end, [
- state.schema.text(' '),
- this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
- ]);
+ if (emoji) {
+ tr.replaceWith(from, to, [
+ state.schema.text(' '),
+ this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
+ ]);
- return tr;
- }
+ return tr;
+ }
- return null;
+ return null;
+ },
}),
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/frontmatter.js b/app/assets/javascripts/content_editor/extensions/frontmatter.js
index 64c84fe046b..c09c10bc524 100644
--- a/app/assets/javascripts/content_editor/extensions/frontmatter.js
+++ b/app/assets/javascripts/content_editor/extensions/frontmatter.js
@@ -17,4 +17,7 @@ export default CodeBlockHighlight.extend({
addNodeView() {
return new VueNodeViewRenderer(FrontmatterWrapper);
},
+ addInputRules() {
+ return [];
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
index c8ec45d835c..c4f31e5f981 100644
--- a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
+++ b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
@@ -1,10 +1,10 @@
import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
-export const hrInputRuleRegExp = /^---$/;
-
export default HorizontalRule.extend({
addInputRules() {
- return [nodeInputRule(hrInputRuleRegExp, this.type)];
+ const hrInputRuleRegExp = /^---$/;
+
+ return [nodeInputRule({ find: hrInputRuleRegExp, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 54adb9efa0c..3abf0e3eee2 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -60,7 +60,13 @@ export default marks.map((name) =>
},
addInputRules() {
- return [markInputRule(markInputRegex(name), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex(name),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
}),
);
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
index 3bd328958df..22bb1ac072e 100644
--- a/app/assets/javascripts/content_editor/extensions/inline_diff.js
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -1,8 +1,5 @@
import { Mark, markInputRule, mergeAttributes } from '@tiptap/core';
-export const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
-export const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
-
export default Mark.create({
name: 'inlineDiff',
@@ -38,9 +35,20 @@ export default Mark.create({
},
addInputRules() {
+ const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
+ const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
+
return [
- markInputRule(inputRegexAddition, this.type, () => ({ type: 'addition' })),
- markInputRule(inputRegexDeletion, this.type, () => ({ type: 'deletion' })),
+ markInputRule({
+ find: inputRegexAddition,
+ type: this.type,
+ getAttributes: () => ({ type: 'addition' }),
+ }),
+ markInputRule({
+ find: inputRegexDeletion,
+ type: this.type,
+ getAttributes: () => ({ type: 'deletion' }),
+ }),
];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index fc0f38e6935..27bc05dce6f 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -1,9 +1,6 @@
import { markInputRule } from '@tiptap/core';
import { Link } from '@tiptap/extension-link';
-export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
-export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
-
const extractHrefFromMatch = (match) => {
return { href: match.groups.href };
};
@@ -26,9 +23,20 @@ export default Link.extend({
openOnClick: false,
},
addInputRules() {
+ const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
+ const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
+
return [
- markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink),
- markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch),
+ markInputRule({
+ find: markdownLinkSyntaxInputRuleRegExp,
+ type: this.type,
+ getAttributes: extractHrefFromMarkdownLink,
+ }),
+ markInputRule({
+ find: urlSyntaxRegExp,
+ type: this.type,
+ getAttributes: extractHrefFromMatch,
+ }),
];
},
addAttributes() {
diff --git a/app/assets/javascripts/content_editor/extensions/math_inline.js b/app/assets/javascripts/content_editor/extensions/math_inline.js
index 60f5288dcf6..4844f6feb29 100644
--- a/app/assets/javascripts/content_editor/extensions/math_inline.js
+++ b/app/assets/javascripts/content_editor/extensions/math_inline.js
@@ -2,8 +2,6 @@ import { Mark, markInputRule } from '@tiptap/core';
import { __ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-export const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
-
export default Mark.create({
name: 'mathInline',
@@ -30,6 +28,8 @@ export default Mark.create({
},
addInputRules() {
- return [markInputRule(inputRegex, this.type)];
+ const inputRegex = /(?:^|\s)\$`([^`]+)`\$$/gm;
+
+ return [markInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/subscript.js b/app/assets/javascripts/content_editor/extensions/subscript.js
index d0766f42308..a8c087e8bf0 100644
--- a/app/assets/javascripts/content_editor/extensions/subscript.js
+++ b/app/assets/javascripts/content_editor/extensions/subscript.js
@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Subscript.extend({
addInputRules() {
- return [markInputRule(markInputRegex('sub'), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex('sub'),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/superscript.js b/app/assets/javascripts/content_editor/extensions/superscript.js
index 6cd814977ea..b86906f01f2 100644
--- a/app/assets/javascripts/content_editor/extensions/superscript.js
+++ b/app/assets/javascripts/content_editor/extensions/superscript.js
@@ -4,6 +4,12 @@ import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark
export default Superscript.extend({
addInputRules() {
- return [markInputRule(markInputRegex('sup'), this.type, extractMarkAttributesFromMatch)];
+ return [
+ markInputRule({
+ find: markInputRegex('sup'),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js
index 0f0477cba2e..004bb8b815c 100644
--- a/app/assets/javascripts/content_editor/extensions/table.js
+++ b/app/assets/javascripts/content_editor/extensions/table.js
@@ -1 +1,42 @@
-export { Table as default } from '@tiptap/extension-table';
+import { Table } from '@tiptap/extension-table';
+import { debounce } from 'lodash';
+import { __ } from '~/locale';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+import { shouldRenderHTMLTable } from '../services/serialization_helpers';
+
+let alertShown = false;
+const onUpdate = debounce((editor) => {
+ if (alertShown) return;
+
+ editor.state.doc.descendants((node) => {
+ if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) {
+ editor.emit('alert', {
+ message: __(
+ 'The content editor may change the markdown formatting style of the document, which may not match your original markdown style.',
+ ),
+ variant: 'warning',
+ });
+
+ alertShown = true;
+
+ return false;
+ }
+
+ return true;
+ });
+}, 1000);
+
+export default Table.extend({
+ addAttributes() {
+ return {
+ isMarkdown: {
+ default: null,
+ parseHTML: (element) => Boolean(getMarkdownSource(element)),
+ },
+ };
+ },
+
+ onUpdate({ editor }) {
+ onUpdate(editor);
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index befc33e669f..9f437ce066c 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,10 +1,9 @@
import { TableCell } from '@tiptap/extension-table-cell';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
-import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
- content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ content: 'block+',
addNodeView() {
return VueNodeViewRenderer(TableCellBodyWrapper);
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 829b06fc14b..045fd03199b 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,10 +1,9 @@
import { TableHeader } from '@tiptap/extension-table-header';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
-import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
- content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ content: 'block+',
addNodeView() {
return VueNodeViewRenderer(TableCellHeaderWrapper);
},
diff --git a/app/assets/javascripts/content_editor/extensions/table_of_contents.js b/app/assets/javascripts/content_editor/extensions/table_of_contents.js
index 9e31158837e..a8882f9ede4 100644
--- a/app/assets/javascripts/content_editor/extensions/table_of_contents.js
+++ b/app/assets/javascripts/content_editor/extensions/table_of_contents.js
@@ -1,10 +1,7 @@
-import { Node } from '@tiptap/core';
-import { InputRule } from 'prosemirror-inputrules';
+import { Node, InputRule } from '@tiptap/core';
import { s__ } from '~/locale';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
-export const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
-
export default Node.create({
name: 'tableOfContents',
@@ -34,17 +31,21 @@ export default Node.create({
addInputRules() {
const { type } = this;
+ const inputRuleRegExps = [/^\[\[_TOC_\]\]$/, /^\[TOC\]$/];
return inputRuleRegExps.map(
(regex) =>
- new InputRule(regex, (state, match, start, end) => {
- const { tr } = state;
+ new InputRule({
+ find: regex,
+ handler: ({ state, range: { from, to }, match }) => {
+ const { tr } = state;
- if (match) {
- tr.replaceWith(start - 1, end, type.create());
- }
+ if (match) {
+ tr.replaceWith(from - 1, to, type.create());
+ }
- return tr;
+ return tr;
+ },
}),
);
},
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index 93b42466850..fa7e02f8cc8 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -1,7 +1,5 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
-export const inputRegex = /^<wbr>$/;
-
export default Node.create({
name: 'wordBreak',
inline: true,
@@ -24,6 +22,8 @@ export default Node.create({
},
addInputRules() {
- return [nodeInputRule(inputRegex, this.type)];
+ const inputRegex = /^<wbr>$/;
+
+ return [nodeInputRule({ find: inputRegex, type: this.type })];
},
});
diff --git a/app/assets/javascripts/content_editor/services/feature_flags.js b/app/assets/javascripts/content_editor/services/feature_flags.js
deleted file mode 100644
index 5f7a4595938..00000000000
--- a/app/assets/javascripts/content_editor/services/feature_flags.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export function isBlockTablesFeatureEnabled() {
- return gon.features?.contentEditorBlockTables;
-}
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index b2327555b45..ed5910fca18 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -1,5 +1,4 @@
import { uniq } from 'lodash';
-import { isBlockTablesFeatureEnabled } from './feature_flags';
const defaultAttrs = {
td: { colspan: 1, rowspan: 1, colwidth: null },
@@ -75,7 +74,7 @@ function getChildren(node) {
return children;
}
-function shouldRenderHTMLTable(table) {
+export function shouldRenderHTMLTable(table) {
const { rows, cells } = getRowsAndCells(table);
const cellChildCount = Math.max(...cells.map((cell) => cell.childCount));
@@ -282,11 +281,6 @@ export function renderOrderedList(state, node) {
}
export function renderTableCell(state, node) {
- if (!isBlockTablesFeatureEnabled()) {
- state.renderInline(node);
- return;
- }
-
if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
state.renderInline(node.child(0));
} else {
@@ -303,9 +297,7 @@ export function renderTableRow(state, node) {
}
export function renderTable(state, node) {
- if (isBlockTablesFeatureEnabled()) {
- setIsInBlockTable(node, shouldRenderHTMLTable(node));
- }
+ setIsInBlockTable(node, shouldRenderHTMLTable(node));
if (isInBlockTable(node)) renderTagOpen(state, 'table');
@@ -317,9 +309,7 @@ export function renderTable(state, node) {
state.closeBlock(node);
state.flushClose();
- if (isBlockTablesFeatureEnabled()) {
- unsetIsInBlockTable(node);
- }
+ unsetIsInBlockTable(node);
}
export function renderHardBreak(state, node, parent, index) {
diff --git a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
index d26f32a7e7a..9b1cb76f845 100644
--- a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
+++ b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
@@ -1,5 +1,5 @@
import { mapValues } from 'lodash';
-import { InputRule } from 'prosemirror-inputrules';
+import { InputRule } from '@tiptap/core';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import Tracking from '~/tracking';
import {
@@ -17,17 +17,20 @@ const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => {
};
const trackInputRule = (contentType, inputRule) => {
- return new InputRule(inputRule.match, (...args) => {
- const result = inputRule.handler(...args);
+ return new InputRule({
+ find: inputRule.find,
+ handler: (...args) => {
+ const result = inputRule.handler(...args);
- if (result) {
- Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
- label: CONTENT_EDITOR_TRACKING_LABEL,
- property: contentType,
- });
- }
+ if (result !== null) {
+ Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property: contentType,
+ });
+ }
- return result;
+ return result;
+ },
});
};
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index 8ac3f719309..f5bf2742748 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -72,8 +72,9 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
- editor.emit('error', {
- error: __('An error occurred while uploading the image. Please try again.'),
+ editor.emit('alert', {
+ message: __('An error occurred while uploading the image. Please try again.'),
+ variant: 'danger',
});
}
};
@@ -102,8 +103,9 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
- editor.emit('error', {
- error: __('An error occurred while uploading the file. Please try again.'),
+ editor.emit('alert', {
+ message: __('An error occurred while uploading the file. Please try again.'),
+ variant: 'danger',
});
}
};
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 08cf0197993..08942374120 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -2,6 +2,8 @@ import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
import { debounce } from 'lodash';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { parseBoolean } from '~/lib/utils/common_utils';
export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
@@ -112,5 +114,8 @@ export default class ContextualSidebar {
const collapse = parseBoolean(Cookies.get('sidebar_collapsed'));
this.toggleCollapsedSidebar(collapse, true);
}
+
+ initInviteMembersModal();
+ initInviteMembersTrigger();
}
}
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 9d4eddc510a..73458a463f2 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -84,7 +84,7 @@ export default {
),
subnetDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#subnets',
securityGroupDropdownHelpText: s__(
- 'ClusterIntegration|Choose the %{linkStart}security group %{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
+ 'ClusterIntegration|Choose the %{linkStart}security group%{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
),
securityGroupDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#securityGroups',
instanceTypesDropdownHelpText: s__(
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index f4a27dc7d1f..ae6e6bf02e4 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -54,6 +54,7 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
this.isGettingRef = false;
+ this.refCancelToken = null;
this.mergeRequestCreated = false;
this.refDebounce = debounce((value, target) => this.getRef(value, target), 500);
this.refIsValid = true;
@@ -101,9 +102,18 @@ export default class CreateMergeRequestDropdown {
'click',
this.onClickCreateMergeRequestButton.bind(this),
);
+ this.branchInput.addEventListener('input', this.onChangeInput.bind(this));
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
+ // Detect for example when user pastes ref using the mouse
+ this.refInput.addEventListener('input', this.onChangeInput.bind(this));
+ // Detect for example when user presses right arrow to apply the suggested ref
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ // Detect when user clicks inside the input to apply the suggested ref
+ this.refInput.addEventListener('click', this.onChangeInput.bind(this));
+ // Detect when user clicks outside the input to apply the suggested ref
+ this.refInput.addEventListener('blur', this.onChangeInput.bind(this));
+ // Detect when user presses tab to apply the suggested ref
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
@@ -247,8 +257,12 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
+ this.refCancelToken = axios.CancelToken.source();
+
return axios
- .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
+ .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`, {
+ cancelToken: this.refCancelToken.token,
+ })
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -267,7 +281,10 @@ export default class CreateMergeRequestDropdown {
return this.updateInputState(target, ref, result);
})
- .catch(() => {
+ .catch((thrown) => {
+ if (axios.isCancel(thrown)) {
+ return false;
+ }
this.unavailable();
this.disable();
createFlash({
@@ -325,14 +342,23 @@ export default class CreateMergeRequestDropdown {
let target;
let value;
+ // User changed input, cancel to prevent previous request from interfering
+ if (this.refCancelToken !== null) {
+ this.refCancelToken.cancel();
+ }
+
if (event.target === this.branchInput) {
target = 'branch';
({ value } = this.branchInput);
} else if (event.target === this.refInput) {
target = 'ref';
- value =
- event.target.value.slice(0, event.target.selectionStart) +
- event.target.value.slice(event.target.selectionEnd);
+ if (event.target === document.activeElement) {
+ value =
+ event.target.value.slice(0, event.target.selectionStart) +
+ event.target.value.slice(event.target.selectionEnd);
+ } else {
+ value = event.target.value;
+ }
} else {
return false;
}
@@ -358,6 +384,7 @@ export default class CreateMergeRequestDropdown {
this.enable();
this.showAvailableMessage(target);
+ this.refDebounce(value, target);
return true;
}
@@ -414,7 +441,8 @@ export default class CreateMergeRequestDropdown {
if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
event.preventDefault();
- window.getSelection().removeAllRanges();
+ const caretPositionEnd = this.refInput.value.length;
+ this.refInput.setSelectionRange(caretPositionEnd, caretPositionEnd);
}
removeMessage(target) {
diff --git a/app/assets/javascripts/crm/components/contacts_root.vue b/app/assets/javascripts/crm/components/contacts_root.vue
new file mode 100644
index 00000000000..83c02f7d5fe
--- /dev/null
+++ b/app/assets/javascripts/crm/components/contacts_root.vue
@@ -0,0 +1,80 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__, __ } from '~/locale';
+import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ inject: ['groupFullPath'],
+ data() {
+ return { contacts: [] };
+ },
+ apollo: {
+ contacts: {
+ query() {
+ return getGroupContactsQuery;
+ },
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ update(data) {
+ return this.extractContacts(data);
+ },
+ error(error) {
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ error,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.contacts.loading;
+ },
+ },
+ methods: {
+ extractContacts(data) {
+ const contacts = data?.group?.contacts?.nodes || [];
+ return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
+ },
+ },
+ fields: [
+ { key: 'firstName', sortable: true },
+ { key: 'lastName', sortable: true },
+ { key: 'email', sortable: true },
+ { key: 'phone', sortable: true },
+ { key: 'description', sortable: true },
+ {
+ key: 'organization',
+ formatter: (organization) => {
+ return organization?.name;
+ },
+ sortable: true,
+ },
+ ],
+ i18n: {
+ emptyText: s__('Crm|No contacts found'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <gl-table
+ v-else
+ :items="contacts"
+ :fields="$options.fields"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/crm/components/organizations_root.vue b/app/assets/javascripts/crm/components/organizations_root.vue
new file mode 100644
index 00000000000..98b45d0a042
--- /dev/null
+++ b/app/assets/javascripts/crm/components/organizations_root.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__, __ } from '~/locale';
+import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ inject: ['groupFullPath'],
+ data() {
+ return { organizations: [] };
+ },
+ apollo: {
+ organizations: {
+ query() {
+ return getGroupOrganizationsQuery;
+ },
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ update(data) {
+ return this.extractOrganizations(data);
+ },
+ error(error) {
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ error,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.organizations.loading;
+ },
+ },
+ methods: {
+ extractOrganizations(data) {
+ const organizations = data?.group?.organizations?.nodes || [];
+ return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
+ },
+ },
+ fields: [
+ { key: 'name', sortable: true },
+ { key: 'defaultRate', sortable: true },
+ { key: 'description', sortable: true },
+ ],
+ i18n: {
+ emptyText: s__('Crm|No organizations found'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <gl-table
+ v-else
+ :items="organizations"
+ :fields="$options.fields"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
new file mode 100644
index 00000000000..f6acd258585
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql
@@ -0,0 +1,22 @@
+query contacts($groupFullPath: ID!) {
+ group(fullPath: $groupFullPath) {
+ __typename
+ id
+ contacts {
+ nodes {
+ __typename
+ id
+ firstName
+ lastName
+ email
+ phone
+ description
+ organization {
+ __typename
+ id
+ name
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
new file mode 100644
index 00000000000..7c4ec6ec585
--- /dev/null
+++ b/app/assets/javascripts/crm/components/queries/get_group_organizations.query.graphql
@@ -0,0 +1,15 @@
+query organizations($groupFullPath: ID!) {
+ group(fullPath: $groupFullPath) {
+ __typename
+ id
+ organizations {
+ nodes {
+ __typename
+ id
+ name
+ defaultRate
+ description
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/crm/contacts_bundle.js b/app/assets/javascripts/crm/contacts_bundle.js
new file mode 100644
index 00000000000..6438953596e
--- /dev/null
+++ b/app/assets/javascripts/crm/contacts_bundle.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import CrmContactsRoot from './components/contacts_root.vue';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.getElementById('js-crm-contacts-app');
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { groupFullPath: el.dataset.groupFullPath },
+ render(createElement) {
+ return createElement(CrmContactsRoot);
+ },
+ });
+};
diff --git a/app/assets/javascripts/crm/organizations_bundle.js b/app/assets/javascripts/crm/organizations_bundle.js
new file mode 100644
index 00000000000..ac9990b9fb4
--- /dev/null
+++ b/app/assets/javascripts/crm/organizations_bundle.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import CrmOrganizationsRoot from './components/organizations_root.vue';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.getElementById('js-crm-organizations-app');
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { groupFullPath: el.dataset.groupFullPath },
+ render(createElement) {
+ return createElement(CrmOrganizationsRoot);
+ },
+ });
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 1d98a42ce58..36430e51dd2 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -2,10 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
@@ -19,6 +21,7 @@ export default {
StageTable,
ValueStreamFilters,
ValueStreamMetrics,
+ UrlSync,
},
props: {
noDataSvgPath: {
@@ -54,6 +57,9 @@ export default {
'pagination',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
+ isLoaded() {
+ return !this.isLoading && !this.isLoadingStage;
+ },
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
@@ -98,6 +104,16 @@ export default {
metricsRequests() {
return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
},
+ query() {
+ return {
+ created_after: toYmd(this.createdAfter),
+ created_before: toYmd(this.createdBefore),
+ stage_id: this.selectedStage?.id || null,
+ sort: this.pagination?.sort || null,
+ direction: this.pagination?.direction || null,
+ page: this.pagination?.page || null,
+ };
+ },
},
methods: {
...mapActions([
@@ -176,5 +192,6 @@ export default {
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/>
+ <url-sync v-if="isLoaded" :query="query" />
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/metric_popover.vue b/app/assets/javascripts/cycle_analytics/components/metric_popover.vue
new file mode 100644
index 00000000000..8d90e7b2392
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/metric_popover.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlPopover, GlLink, GlIcon } from '@gitlab/ui';
+
+export default {
+ name: 'MetricPopover',
+ components: {
+ GlPopover,
+ GlLink,
+ GlIcon,
+ },
+ props: {
+ metric: {
+ type: Object,
+ required: true,
+ },
+ target: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ metricLinks() {
+ return this.metric.links?.filter((link) => !link.docs_link) || [];
+ },
+ docsLink() {
+ return this.metric.links?.find((link) => link.docs_link);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover :target="target" placement="bottom">
+ <template #title>
+ <span class="gl-display-block gl-text-left" data-testid="metric-label">{{
+ metric.label
+ }}</span>
+ </template>
+ <div
+ v-for="(link, idx) in metricLinks"
+ :key="`link-${idx}`"
+ class="gl-display-flex gl-justify-content-space-between gl-text-right gl-py-1"
+ data-testid="metric-link"
+ >
+ <span>{{ link.label }}</span>
+ <gl-link :href="link.url" class="gl-font-sm">
+ {{ link.name }}
+ </gl-link>
+ </div>
+ <span v-if="metric.description" data-testid="metric-description">{{ metric.description }}</span>
+ <gl-link
+ v-if="docsLink"
+ :href="docsLink.url"
+ class="gl-font-sm"
+ target="_blank"
+ data-testid="metric-docs-link"
+ >{{ docsLink.label }}
+ <gl-icon name="external-link" class="gl-vertical-align-middle" />
+ </gl-link>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
index 7371ffd2c7c..9671742e564 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
@@ -1,11 +1,13 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlPopover } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { flatten } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
+import MetricPopover from './metric_popover.vue';
const requestData = ({ request, endpoint, path, params, name }) => {
return request({ endpoint, params, requestPath: path })
@@ -31,9 +33,9 @@ const fetchMetricsData = (reqs = [], path, params) => {
export default {
name: 'ValueStreamMetrics',
components: {
- GlPopover,
GlSingleStat,
GlSkeletonLoading,
+ MetricPopover,
},
props: {
requestPath: {
@@ -76,32 +78,33 @@ export default {
this.isLoading = false;
});
},
+ hasLinks(links) {
+ return links?.length && links[0].url;
+ },
+ clickHandler({ links }) {
+ if (this.hasLinks(links)) {
+ redirectTo(links[0].url);
+ }
+ },
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-wrap" data-testid="vsa-time-metrics">
- <div v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6">
- <gl-skeleton-loading />
+ <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
+ <div v-for="metric in metrics" v-show="!isLoading" :key="metric.key" class="gl-my-6 gl-pr-9">
+ <gl-single-stat
+ :id="metric.key"
+ :value="`${metric.value}`"
+ :title="metric.label"
+ :unit="metric.unit || ''"
+ :should-animate="true"
+ :animation-decimal-places="1"
+ :class="{ 'gl-hover-cursor-pointer': hasLinks(metric.links) }"
+ tabindex="0"
+ @click="clickHandler(metric)"
+ />
+ <metric-popover :metric="metric" :target="metric.key" />
</div>
- <template v-else>
- <div v-for="metric in metrics" :key="metric.key" class="gl-my-6 gl-pr-9">
- <gl-single-stat
- :id="metric.key"
- :value="`${metric.value}`"
- :title="metric.label"
- :unit="metric.unit || ''"
- :should-animate="true"
- :animation-decimal-places="1"
- tabindex="0"
- />
- <gl-popover :target="metric.key" placement="bottom">
- <template #title>
- <span class="gl-display-block gl-text-left">{{ metric.label }}</span>
- </template>
- <span v-if="metric.description">{{ metric.description }}</span>
- </gl-popover>
- </div>
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index c205aa1e831..7d5822b0824 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -5,8 +5,6 @@ import {
} from '~/api/analytics_api';
import { __, s__ } from '~/locale';
-export const DEFAULT_DAYS_IN_PAST = 30;
-export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
@@ -47,6 +45,11 @@ export const METRICS_POPOVER_CONTENT = {
"ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
),
},
+ 'lead-time-for-changes': {
+ description: s__(
+ 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.',
+ ),
+ },
'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') },
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 34ef03409b8..3da8696edeb 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -1,44 +1,36 @@
import Vue from 'vue';
+import {
+ extractFilterQueryParameters,
+ extractPaginationQueryParameters,
+} from '~/analytics/shared/utils';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
-import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
-import { calculateFormattedDayInPast } from './utils';
+import { buildCycleAnalyticsInitialData } from './utils';
Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const {
- noAccessSvgPath,
- noDataSvgPath,
- requestPath,
- fullPath,
- projectId,
- groupId,
- groupPath,
- labelsPath,
- milestonesPath,
- } = el.dataset;
+ const { noAccessSvgPath, noDataSvgPath } = el.dataset;
+ const initialData = buildCycleAnalyticsInitialData({ ...el.dataset, gon });
- const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ const pagination = extractPaginationQueryParameters(window.location.search);
+ const {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ } = extractFilterQueryParameters(window.location.search);
store.dispatch('initializeVsa', {
- projectId: parseInt(projectId, 10),
- endpoints: {
- requestPath,
- fullPath,
- labelsPath,
- milestonesPath,
- groupId: parseInt(groupId, 10),
- groupPath,
- },
- features: {
- cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
- },
- createdBefore: new Date(now),
- createdAfter: new Date(past),
+ ...initialData,
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ pagination,
});
// eslint-disable-next-line no-new
@@ -52,7 +44,6 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
- fullPath,
},
}),
});
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 24b62849db7..e0156b24f9d 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -14,7 +14,7 @@ import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
- return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
+ return dispatch('fetchValueStreamStages');
};
export const fetchValueStreamStages = ({ commit, state }) => {
@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state;
commit(types.REQUEST_VALUE_STREAMS);
- const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({
});
};
-export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
- const stage = selectedStage || stages[0];
- commit(types.SET_SELECTED_STAGE, stage);
- return dispatch('fetchStageData');
+export const fetchValueStreamStageData = ({ dispatch }) =>
+ Promise.all([
+ dispatch('fetchCycleAnalyticsData'),
+ dispatch('fetchStageData'),
+ dispatch('fetchStageMedians'),
+ dispatch('fetchStageCountValues'),
+ ]);
+
+export const refetchStageData = async ({ dispatch, commit }) => {
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreamStageData');
+ commit(types.SET_LOADING, false);
};
-export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
-
-const refetchStageData = (dispatch) => {
- return Promise.resolve()
- .then(() => dispatch('setLoading', true))
- .then(() =>
- Promise.all([
- dispatch('fetchCycleAnalyticsData'),
- dispatch('fetchStageData'),
- dispatch('fetchStageMedians'),
- dispatch('fetchStageCountValues'),
- ]),
- )
- .finally(() => dispatch('setLoading', false));
+export const setSelectedStage = ({ dispatch, commit }, selectedStage = null) => {
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('refetchStageData');
};
-export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
+export const setFilters = ({ dispatch }) => dispatch('refetchStageData');
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
- return refetchStageData(dispatch);
+ return dispatch('refetchStageData');
+};
+
+export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
+ const selectedStage = stage || stages[0];
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('fetchValueStreamStageData');
};
export const updateStageTablePagination = (
@@ -190,12 +191,18 @@ export const updateStageTablePagination = (
return dispatch('fetchStageData', selectedStage.id);
};
-export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
+export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ selectedStage = null,
} = initialData;
+
dispatch('filters/setEndpoints', {
labelsEndpoint: labelsPath,
milestonesEndpoint: milestonesPath,
@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
projectEndpoint: fullPath,
});
- return dispatch('setLoading', true)
- .then(() => dispatch('fetchValueStreams'))
- .finally(() => dispatch('setLoading', false));
+ dispatch('filters/initialize', {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ });
+
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreams');
+ await dispatch('setInitialStage', selectedStage);
+ commit(types.SET_LOADING, false);
};
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 3c6267bac06..9af63f5f9cc 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,7 +1,4 @@
-import dateFormat from 'dateformat';
-import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash';
-import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility';
@@ -74,23 +71,6 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
-const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
-
-/**
- * Takes an integer specifying the number of days to subtract
- * from the date specified will return the 2 dates, formatted as ISO dates
- *
- * @param {Number} daysInPast - Number of days in the past to subtract
- * @param {Date} [today=new Date] - Date to subtract days from, defaults to today
- * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
- */
-export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
- return {
- now: toIsoFormat(today),
- past: toIsoFormat(getDateInPast(today, daysInPast)),
- };
-};
-
/**
* @typedef {Object} MetricData
* @property {String} title - Title of the metric measured
@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '',
};
});
+
+const extractFeatures = (gon) => ({
+ cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
+});
+
+/**
+ * Builds the initial data object for Value Stream Analytics with data loaded from the backend
+ *
+ * @param {Object} dataset - dataset object paseed to the frontend via data-* properties
+ * @returns {Object} - The initial data to load the app with
+ */
+export const buildCycleAnalyticsInitialData = ({
+ fullPath,
+ requestPath,
+ projectId,
+ groupId,
+ groupPath,
+ labelsPath,
+ milestonesPath,
+ stage,
+ createdAfter,
+ createdBefore,
+ gon,
+} = {}) => {
+ return {
+ projectId: parseInt(projectId, 10),
+ endpoints: {
+ requestPath,
+ fullPath,
+ labelsPath,
+ milestonesPath,
+ groupId: parseInt(groupId, 10),
+ groupPath,
+ },
+ createdAfter: new Date(createdAfter),
+ createdBefore: new Date(createdBefore),
+ selectedStage: stage ? JSON.parse(stage) : null,
+ features: extractFeatures(gon),
+ };
+};
diff --git a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
index e026391ae22..fdf8b7796bf 100644
--- a/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
+++ b/app/assets/javascripts/deploy_tokens/components/revoke_button.vue
@@ -62,7 +62,7 @@ export default {
</gl-sprintf>
{{ s__('DeployTokens|This action cannot be undone.') }}
<template #modal-footer>
- <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button>
<gl-button
category="primary"
variant="danger"
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index fa57537f74e..5cf32cb7fe3 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -88,7 +88,6 @@ const defaultClient = createDefaultClient(
fragmentMatcher,
},
typeDefs,
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 5092c30aa60..42d5d8fb359 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -4,7 +4,7 @@ import VueDraggable from 'vuedraggable';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { getFilename } from '~/lib/utils/file_upload';
+import { getFilename, validateImageName } from '~/lib/utils/file_upload';
import { __, s__, sprintf } from '~/locale';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import DeleteButton from '../components/delete_button.vue';
@@ -284,12 +284,16 @@ export default {
return;
}
event.preventDefault();
- let filename = getFilename(event);
- if (!filename || filename === 'image.png') {
- filename = `design_${Date.now()}.png`;
- }
- const newFile = new File([files[0]], filename);
- this.onUploadDesign([newFile]);
+ const fileList = [...files];
+ fileList.forEach((file) => {
+ let filename = getFilename(file);
+ filename = validateImageName(file);
+ if (!filename || filename === 'image.png') {
+ filename = `design_${Date.now()}.png`;
+ }
+ const newFile = new File([file], filename);
+ this.onUploadDesign([newFile]);
+ });
}
},
toggleOnPasteListener() {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 465f9836140..f405b82b05b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -44,6 +44,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
+import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -86,6 +87,9 @@ export default {
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
},
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
props: {
endpoint: {
type: String,
@@ -392,8 +396,6 @@ export default {
diffsApp.instrument();
},
created() {
- this.mergeRequestContainers = document.querySelectorAll('.merge-request-container');
-
this.adjustView();
this.subscribeToEvents();
@@ -521,13 +523,6 @@ export default {
} else {
this.removeEventListeners();
}
-
- if (!this.isFluidLayout && this.glFeatures.mrChangesFluidLayout) {
- this.mergeRequestContainers.forEach((el) => {
- el.classList.toggle('limit-container-width', !this.shouldShow);
- el.classList.toggle('container-limited', !this.shouldShow);
- });
- }
},
setEventListeners() {
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
@@ -579,7 +574,7 @@ export default {
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
if (targetIndex >= 0 && targetIndex < this.diffFiles.length) {
- this.scrollToFile(this.diffFiles[targetIndex].file_path);
+ this.scrollToFile({ path: this.diffFiles[targetIndex].file_path });
}
},
setTreeDisplay() {
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 4435a533591..e54fde72847 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -34,6 +34,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -88,6 +89,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
};
</script>
@@ -101,7 +105,7 @@ export default {
>
<div
v-if="commit.signature_html"
- v-html="commit.signature_html /* eslint-disable-line vue/no-v-html */"
+ v-safe-html:[$options.safeHtmlConfig]="commit.signature_html"
></div>
<commit-pipeline-status
v-if="commit.pipeline_status_path"
@@ -142,9 +146,9 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content" data-qa-selector="commit_content">
<a
+ v-safe-html:[$options.safeHtmlConfig]="commit.title_html"
:href="commit.commit_url"
class="commit-row-message item-title"
- v-html="commit.title_html /* eslint-disable-line vue/no-v-html */"
></a>
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
@@ -174,9 +178,9 @@ export default {
<div>
<pre
v-if="commit.description_html"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 gl-text-body"
- v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_comment_cell.vue b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
index 4af4b46f94c..a4fae652d02 100644
--- a/app/assets/javascripts/diffs/components/diff_comment_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ lineRange: {
+ type: Object,
+ required: false,
+ default: null,
+ },
linePosition: {
type: String,
required: false,
@@ -59,6 +64,7 @@ export default {
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
+ :range="lineRange"
:note-target-line="line"
:help-page-path="helpPagePath"
:line-position="linePosition"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 4bcb99424db..238f07ac22c 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -14,7 +14,6 @@ import {
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
-import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
@@ -50,7 +49,7 @@ export default {
mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })],
i18n: {
...DIFF_FILE_HEADER,
- compareButtonLabel: s__('Compare submodule commit revisions'),
+ compareButtonLabel: __('Compare submodule commit revisions'),
},
props: {
discussionPath: {
@@ -130,7 +129,7 @@ export default {
const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha));
const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha));
return sprintf(
- s__('Compare %{oldCommitId}...%{newCommitId}'),
+ __('Compare %{oldCommitId}...%{newCommitId}'),
{
oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`,
newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`,
@@ -181,7 +180,7 @@ export default {
return this.diffFile.renamed_file;
},
isModeChanged() {
- return this.diffFile.viewer.name === diffViewerModes.mode_changed;
+ return this.diffFile.mode_changed;
},
expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) {
@@ -221,7 +220,7 @@ export default {
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
- 'toggleActiveFileByHash',
+ 'setCurrentFileHash',
'reviewFile',
'setFileCollapsedByUser',
]),
@@ -244,7 +243,7 @@ export default {
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
if (!this.viewDiffsFileByFile) {
- this.toggleActiveFileByHash(this.diffFile.file_hash);
+ this.setCurrentFileHash(this.diffFile.file_hash);
}
}
},
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 c445989f143..9d355c96af1 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -2,6 +2,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import {
@@ -32,6 +33,11 @@ export default {
type: Object,
required: true,
},
+ range: {
+ type: Object,
+ required: false,
+ default: null,
+ },
linePosition: {
type: String,
required: false,
@@ -49,6 +55,7 @@ export default {
},
data() {
return {
+ lines: null,
commentLineStart: {
line_code: this.line.line_code,
type: this.line.type,
@@ -116,10 +123,8 @@ export default {
return commentLineOptions(lines, this.line, this.line.line_code, side);
},
commentLines() {
- if (!this.selectedCommentPosition) return [];
-
const lines = [];
- const { start, end } = this.selectedCommentPosition;
+ const { start, end } = this.lines;
const diffLines = this.diffFile[INLINE_DIFF_LINES_KEY];
let isAdding = false;
@@ -144,6 +149,13 @@ export default {
return lines;
},
},
+ created() {
+ if (this.range) {
+ this.lines = { ...this.range };
+ } else if (this.line) {
+ this.lines = { start: this.line, end: this.line };
+ }
+ },
mounted() {
if (this.isLoggedIn) {
const keys = [
@@ -166,16 +178,16 @@ export default {
'saveDiffDiscussion',
'setSuggestPopoverDismissed',
]),
- handleCancelCommentForm(shouldConfirm, isDirty) {
+ async handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
- // eslint-disable-next-line no-alert
- if (!window.confirm(msg)) {
+ const confirmed = await confirmAction(msg);
+
+ if (!confirmed) {
return;
}
}
-
this.cancelCommentForm({
lineCode: this.line.line_code,
fileHash: this.diffFileHash,
@@ -189,6 +201,9 @@ export default {
this.handleCancelCommentForm(),
);
},
+ updateStartLine(line) {
+ this.lines.start = line;
+ },
},
};
</script>
@@ -199,7 +214,9 @@ export default {
<multiline-comment-form
v-model="commentLineStart"
:line="line"
+ :line-range="lines"
:comment-line-options="commentLineOptions"
+ @input="updateStartLine"
/>
</div>
<note-form
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 64ded1ca8ca..55c796182ee 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -6,6 +6,7 @@ import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { pickDirection } from '../utils/diff_line';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import DiffRow from './diff_row.vue';
@@ -106,6 +107,16 @@ export default {
});
this.idState.dragStart = null;
},
+ singleLineComment(code, line) {
+ const lineDir = pickDirection({ line, code });
+
+ this.idState.updatedLineRange = {
+ start: lineDir,
+ end: lineDir,
+ };
+
+ this.showCommentForm({ lineCode: lineDir.line_code, fileHash: this.diffFile.file_hash });
+ },
isHighlighted(line) {
return isHighlighted(
this.highlightedRow,
@@ -169,7 +180,7 @@ export default {
:index="index"
:is-highlighted="isHighlighted(line)"
:file-line-coverage="fileLineCoverage"
- @showCommentForm="(lineCode) => showCommentForm({ lineCode, fileHash: diffFile.file_hash })"
+ @showCommentForm="(code) => singleLineComment(code, line)"
@setHighlightedRow="setHighlightedRow"
@toggleLineDiscussions="
({ lineCode, expanded }) =>
@@ -193,6 +204,7 @@ export default {
<diff-comment-cell
v-if="line.left && (line.left.renderDiscussion || line.left.hasCommentForm)"
:line="line.left"
+ :line-range="idState.updatedLineRange"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
line-position="left"
@@ -206,6 +218,7 @@ export default {
<diff-comment-cell
v-if="line.right && (line.right.renderDiscussion || line.right.hasCommentForm)"
:line="line.right"
+ :line-range="idState.updatedLineRange"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 41d885d3dc1..85e4199d1c1 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -98,7 +98,7 @@ export default {
:file-row-component="$options.DiffFileRow"
:current-diff-file-id="currentDiffFileId"
@toggleTreeOpen="toggleTreeOpen"
- @clickFile="scrollToFile"
+ @clickFile="(path) => scrollToFile({ path })"
/>
</template>
<p v-else class="prepend-top-20 append-bottom-20 text-center">
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 1b1ab59b2b4..260ebdf2141 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -138,7 +138,7 @@ export default function initDiffsApp(store) {
...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
openFile(file) {
window.mrTabs.tabShown('diffs');
- this.scrollToFile(file.path);
+ this.scrollToFile({ path: file.path });
},
},
render(createElement) {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5c94c6b803b..692cb913a57 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -85,6 +85,12 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
});
+
+ Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => {
+ const viewedId = id.replace(/^hash:/, '');
+
+ commit(types.SET_DIFF_FILE_VIEWED, { id: viewedId, seen: true });
+ });
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
@@ -127,7 +133,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
}
if (!isNoteLink && !state.currentDiffFileId) {
- commit(types.VIEW_DIFF_FILE, diff_files[0]?.file_hash);
+ commit(types.SET_CURRENT_DIFF_FILE, diff_files[0]?.file_hash);
}
if (isNoteLink) {
@@ -143,7 +149,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
!state.diffFiles.some((f) => f.file_hash === state.currentDiffFileId) &&
!isNoteLink
) {
- commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
+ commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0].file_hash);
}
if (state.diffFiles?.length) {
@@ -248,7 +254,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
handleLocationHash();
};
@@ -514,23 +520,25 @@ export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
-export const toggleActiveFileByHash = ({ commit }, hash) => {
- commit(types.VIEW_DIFF_FILE, hash);
+export const setCurrentFileHash = ({ commit }, hash) => {
+ commit(types.SET_CURRENT_DIFF_FILE, hash);
};
-export const scrollToFile = ({ state, commit, getters }, path) => {
+export const scrollToFile = ({ state, commit, getters }, { path, setHash = true }) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
if (getters.isVirtualScrollingEnabled) {
eventHub.$emit('scrollToFileHash', fileHash);
- setTimeout(() => {
- window.history.replaceState(null, null, `#${fileHash}`);
- });
+ if (setHash) {
+ setTimeout(() => {
+ window.history.replaceState(null, null, `#${fileHash}`);
+ });
+ }
} else {
document.location.hash = fileHash;
@@ -804,7 +812,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
if (fileHash && state.diffFiles.some((f) => f.file_hash === fileHash)) {
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
}
};
@@ -812,7 +820,7 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
const fileHash = state.diffFiles[index].file_hash;
document.location.hash = fileHash;
- commit(types.VIEW_DIFF_FILE, fileHash);
+ commit(types.SET_CURRENT_DIFF_FILE, fileHash);
};
export const setFileByFile = ({ state, commit }, { fileByFile }) => {
@@ -848,6 +856,8 @@ export function reviewFile({ commit, state }, { file, reviewed = true }) {
const reviews = markFileReview(state.mrReviews, file, reviewed);
setReviewsForMergeRequest(mrPath, reviews);
+
+ commit(types.SET_DIFF_FILE_VIEWED, { id: file.file_hash, seen: reviewed });
commit(types.SET_MR_FILE_REVIEWS, reviews);
}
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 60836f747f5..51c21c1bfc4 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -20,7 +20,8 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const SET_SHOW_TREE_LIST = 'SET_SHOW_TREE_LIST';
-export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE';
+export const SET_CURRENT_DIFF_FILE = 'SET_CURRENT_DIFF_FILE';
+export const SET_DIFF_FILE_VIEWED = 'SET_DIFF_FILE_VIEWED';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 6bc927b9d1f..4a9df0eafcc 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -254,9 +254,11 @@ export default {
[types.SET_SHOW_TREE_LIST](state, showTreeList) {
state.showTreeList = showTreeList;
},
- [types.VIEW_DIFF_FILE](state, fileId) {
+ [types.SET_CURRENT_DIFF_FILE](state, fileId) {
state.currentDiffFileId = fileId;
- Vue.set(state.viewedDiffFileIds, fileId, true);
+ },
+ [types.SET_DIFF_FILE_VIEWED](state, { id, seen }) {
+ Vue.set(state.viewedDiffFileIds, id, seen);
},
[types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
state.commentForms.push({
diff --git a/app/assets/javascripts/diffs/utils/diff_line.js b/app/assets/javascripts/diffs/utils/diff_line.js
new file mode 100644
index 00000000000..a248cc6318b
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/diff_line.js
@@ -0,0 +1,10 @@
+export function pickDirection({ line, code } = {}) {
+ const { left, right } = line;
+ let direction = left || right;
+
+ if (right?.line_code === code) {
+ direction = right;
+ }
+
+ return direction;
+}
diff --git a/app/assets/javascripts/diffs/utils/discussions.js b/app/assets/javascripts/diffs/utils/discussions.js
new file mode 100644
index 00000000000..c404705d209
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/discussions.js
@@ -0,0 +1,76 @@
+function normalize(processable) {
+ const { entry } = processable;
+ const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
+ const direction =
+ offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
+
+ return {
+ ...processable,
+ entry: {
+ time: entry.time,
+ type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
+ },
+ };
+}
+
+function sort({ entry: alpha }, { entry: beta }) {
+ const diff = alpha.time - beta.time;
+ let order = 0;
+
+ if (diff < 0) {
+ order = -1;
+ } else if (diff > 0) {
+ order = 1;
+ } else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
+ order = 2;
+ } else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
+ order = -2;
+ }
+
+ return order;
+}
+
+function filter(entry) {
+ return entry.type !== 'scrollDown';
+}
+
+export function discussionIntersectionObserverHandlerFactory() {
+ let unprocessed = [];
+ let timer = null;
+
+ return (processable) => {
+ unprocessed.push(processable);
+
+ if (timer) {
+ clearTimeout(timer);
+ }
+
+ timer = setTimeout(() => {
+ unprocessed
+ .map(normalize)
+ .filter(filter)
+ .sort(sort)
+ .forEach((discussionObservationContainer) => {
+ const {
+ entry: { type },
+ currentDiscussion,
+ isFirstUnresolved,
+ isDiffsPage,
+ functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
+ } = discussionObservationContainer;
+
+ if (type === 'intersection') {
+ setCurrentDiscussionId(currentDiscussion.id);
+ } else if (type === 'scrollUp') {
+ setCurrentDiscussionId(
+ isFirstUnresolved
+ ? null
+ : getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
+ );
+ }
+ });
+
+ unprocessed = [];
+ }, 0);
+ };
+}
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 7a4b1aa6b17..227be4e4a6c 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -52,8 +52,10 @@ export function markFileReview(reviews, file, reviewed = true) {
if (reviewed) {
fileReviews.add(file.id);
+ fileReviews.add(`hash:${file.file_hash}`);
} else {
fileReviews.delete(file.id);
+ fileReviews.delete(`hash:${file.file_hash}`);
}
updatedReviews[file.file_identifier_hash] = Array.from(fileReviews);
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f98f63529fc..f404fa4e0e8 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -43,7 +43,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let pasteText;
let addFileToForm;
let updateAttachingMessage;
- let isImage;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
@@ -173,7 +172,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
return dropzoneInstance.addFile(file);
});
});
- // eslint-disable-next-line consistent-return
+
handlePaste = (event) => {
const pasteEvent = event.originalEvent;
const { clipboardData } = pasteEvent;
@@ -186,32 +185,22 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const text = converter.convertToTableMarkdown();
pasteText(text);
} else {
- const image = isImage(pasteEvent);
-
- if (image) {
- event.preventDefault();
- const MAX_FILE_NAME_LENGTH = 246;
- const filename = getFilename(pasteEvent) || 'image.png';
- const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
- const text = `{{${truncateFilename}}}`;
- pasteText(text);
-
- return uploadFile(image.getAsFile(), truncateFilename);
- }
- }
- }
- };
-
- isImage = (data) => {
- let i = 0;
- while (i < data.clipboardData.items.length) {
- const item = data.clipboardData.items[i];
- if (item.type.indexOf('image') !== -1) {
- return item;
+ const fileList = [...clipboardData.files];
+ fileList.forEach((file) => {
+ if (file.type.indexOf('image') !== -1) {
+ event.preventDefault();
+ const MAX_FILE_NAME_LENGTH = 246;
+
+ const filename = getFilename(file) || 'image.png';
+ const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
+ const text = `{{${truncateFilename}}}`;
+ pasteText(text);
+
+ uploadFile(file, truncateFilename);
+ }
+ });
}
- i += 1;
}
- return false;
};
pasteText = (text, shouldPad) => {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index d40d19000fb..e855e304d27 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,17 +1,9 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { __ } from '~/locale';
-
-export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = __(
- '"el" parameter is required for createInstance()',
-);
+import { s__ } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
-export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
- 'Source Editor instance is required to set up an extension.',
-);
-
export const EDITOR_READY_EVENT = 'editor-ready';
export const EDITOR_TYPE_CODE = 'vs.editor.ICodeEditor';
@@ -20,6 +12,32 @@ export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
export const EDITOR_CODE_INSTANCE_FN = 'createInstance';
export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
+export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = s__(
+ 'SourceEditor|"el" parameter is required for createInstance()',
+);
+export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = s__(
+ 'SourceEditor|Source Editor instance is required to set up an extension.',
+);
+export const EDITOR_EXTENSION_DEFINITION_ERROR = s__(
+ 'SourceEditor|Extension definition should be either a class or a function',
+);
+export const EDITOR_EXTENSION_NO_DEFINITION_ERROR = s__(
+ 'SourceEditor|`definition` property is expected on the extension.',
+);
+export const EDITOR_EXTENSION_DEFINITION_TYPE_ERROR = s__(
+ 'SourceEditor|Extension definition should be either class, function, or an Array of definitions.',
+);
+export const EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR = s__(
+ 'SourceEditor|No extension for unuse has been specified.',
+);
+export const EDITOR_EXTENSION_NOT_REGISTERED_ERROR = s__('SourceEditor|%{name} is not registered.');
+export const EDITOR_EXTENSION_NAMING_CONFLICT_ERROR = s__(
+ 'SourceEditor|Name conflict for "%{prop}()" method.',
+);
+export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
+ 'SourceEditor|Extensions Store is required to check for an extension.',
+);
+
//
// EXTENSIONS' CONSTANTS
//
diff --git a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
new file mode 100644
index 00000000000..119a2aea9eb
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js
@@ -0,0 +1,116 @@
+// THIS IS AN EXAMPLE
+//
+// This file contains a basic documented example of the Source Editor extensions'
+// API for your convenience. You can copy/paste it into your own file
+// and adjust as you see fit
+//
+
+export class MyFancyExtension {
+ /**
+ * THE LIFE-CYCLE CALLBACKS
+ */
+
+ /**
+ * Is called before the extension gets used by an instance,
+ * Use `onSetup` to setup Monaco directly:
+ * actions, keystrokes, update options, etc.
+ * Is called only once before the extension gets registered
+ *
+ * @param { Object } [setupOptions] The setupOptions object
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onSetup(setupOptions, instance) {}
+
+ /**
+ * The first thing called after the extension is
+ * registered and used by an instance.
+ * Is called every time the extension is applied
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onUse(instance) {}
+
+ /**
+ * Is called before un-using an extension. Can be used for time-critical
+ * actions like cleanup, reverting visual changes, and other user-facing
+ * updates.
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onBeforeUnuse(instance) {}
+
+ /**
+ * Is called right after an extension is removed from an instance (un-used)
+ * Can be used for non time-critical tasks like cleanup on the Monaco level
+ * (removing actions, keystrokes, etc.).
+ * onUnuse() will be executed during the browser's idle period
+ * (https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
+ *
+ * @param { Object } [instance] The Source Editor instance
+ */
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
+ onUnuse(instance) {}
+
+ /**
+ * The public API of the extension: these are the methods that will be exposed
+ * to the end user
+ * @returns {Object}
+ */
+ provides() {
+ return {
+ basic: () => {
+ // The most basic method not depending on anything
+ // Use: instance.basic();
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return 'Foo Bar';
+ },
+ basicWithProp: () => {
+ // The methods with access to the props of the extension.
+ // The props can be either hardcoded (for example in `onSetup`), or
+ // can be dynamically passed as part of `setupOptions` object when
+ // using the extension.
+ // Use: instance.use({ definition: MyFancyExtension, setupOptions: { foo: 'bar' }});
+ return this.foo;
+ },
+ basicWithPropsAsList: (prop1, prop2) => {
+ // Just a simple method with local props
+ // The props are passed as usually.
+ // Use: instance.basicWithPropsAsList(prop1, prop2);
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `The prop1 is ${prop1}; the prop2 is ${prop2}`;
+ },
+ basicWithInstance: (instance) => {
+ // The method accessing the instance methods: either own or provided
+ // by previously-registered extensions
+ // `instance` is always supplied to all methods in provides() as THE LAST
+ // argument.
+ // You don't need to explicitly pass instance to this method:
+ // Use: instance.basicWithInstance();
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `We have access to the whole Instance! ${instance.alpha()}`;
+ },
+ advancedWithInstanceAndProps: ({ author, book } = {}, firstname, lastname, instance) => {
+ // Advanced method where
+ // { author, book } — are the props passed as an object
+ // prop1, prop2 — are the props passed as simple list
+ // instance — is automatically supplied, no need to pass it to
+ // the method explicitly
+ // Use: instance.advancedWithInstanceAndProps(
+ // {
+ // author: 'Franz Kafka',
+ // book: 'The Transformation'
+ // },
+ // 'Franz',
+ // 'Kafka'
+ // );
+ return `
+The author is ${author}; the book is ${book}
+The author's name is ${firstname}; the last name is ${lastname}
+We have access to the whole Instance! For example, 'instance.alpha()': ${instance.alpha()}`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 5fa01f03f7e..03c68fed3b1 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -36,12 +36,24 @@ export class SourceEditorExtension {
});
}
- static highlightLines(instance) {
- const { hash } = window.location;
- if (!hash) {
- return;
- }
- const [start, end] = hash.replace(hashRegexp, '').split('-');
+ static removeHighlights(instance) {
+ Object.assign(instance, {
+ lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], []),
+ });
+ }
+
+ /**
+ * Returns a function that can only be invoked once between
+ * each browser screen repaint.
+ * @param {Object} instance - The Source Editor instance
+ * @param {Array} bounds - The [start, end] array with start
+ * and end coordinates for highlighting
+ */
+ static highlightLines(instance, bounds = null) {
+ const [start, end] =
+ bounds && Array.isArray(bounds)
+ ? bounds
+ : window.location.hash?.replace(hashRegexp, '').split('-');
let startLine = start ? parseInt(start, 10) : null;
let endLine = end ? parseInt(end, 10) : startLine;
if (endLine < startLine) {
@@ -51,15 +63,12 @@ export class SourceEditorExtension {
window.requestAnimationFrame(() => {
instance.revealLineInCenter(startLine);
Object.assign(instance, {
- lineDecorations: instance.deltaDecorations(
- [],
- [
- {
- range: new Range(startLine, 1, endLine, 1),
- options: { isWholeLine: true, className: 'active-line-text' },
- },
- ],
- ),
+ lineDecorations: instance.deltaDecorations(instance.lineDecorations || [], [
+ {
+ range: new Range(startLine, 1, endLine, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ]),
});
});
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
new file mode 100644
index 00000000000..212e09c8724
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_yaml_ext.js
@@ -0,0 +1,293 @@
+import { toPath } from 'lodash';
+import { parseDocument, Document, visit, isScalar, isCollection, isMap } from 'yaml';
+import { findPair } from 'yaml/util';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+
+export class YamlEditorExtension extends SourceEditorExtension {
+ /**
+ * Extends the source editor with capabilities for yaml files.
+ *
+ * @param { Instance } instance Source Editor Instance
+ * @param { boolean } enableComments Convert model nodes with the comment
+ * pattern to comments?
+ * @param { string } highlightPath Add a line highlight to the
+ * node specified by this e.g. `"foo.bar[0]"`
+ * @param { * } model Any JS Object that will be stringified and used as the
+ * editor's value. Equivalent to using `setDataModel()`
+ * @param options SourceEditorExtension Options
+ */
+ constructor({
+ instance,
+ enableComments = false,
+ highlightPath = null,
+ model = null,
+ ...options
+ } = {}) {
+ super({
+ instance,
+ options: {
+ ...options,
+ enableComments,
+ highlightPath,
+ },
+ });
+
+ if (model) {
+ YamlEditorExtension.initFromModel(instance, model);
+ }
+
+ instance.onDidChangeModelContent(() => instance.onUpdate());
+ }
+
+ /**
+ * @private
+ */
+ static initFromModel(instance, model) {
+ const doc = new Document(model);
+ if (instance.options.enableComments) {
+ YamlEditorExtension.transformComments(doc);
+ }
+ instance.setValue(doc.toString());
+ }
+
+ /**
+ * @private
+ * This wraps long comments to a maximum line length of 80 chars.
+ *
+ * The `yaml` package does not currently wrap comments. This function
+ * is a local workaround and should be deprecated if
+ * https://github.com/eemeli/yaml/issues/322
+ * is resolved.
+ */
+ static wrapCommentString(string, level = 0) {
+ if (!string) {
+ return null;
+ }
+ if (level < 0 || Number.isNaN(parseInt(level, 10))) {
+ throw Error(`Invalid value "${level}" for variable \`level\``);
+ }
+ const maxLineWidth = 80;
+ const indentWidth = 2;
+ const commentMarkerWidth = '# '.length;
+ const maxLength = maxLineWidth - commentMarkerWidth - level * indentWidth;
+ const lines = [[]];
+ string.split(' ').forEach((word) => {
+ const currentLine = lines.length - 1;
+ if ([...lines[currentLine], word].join(' ').length <= maxLength) {
+ lines[currentLine].push(word);
+ } else {
+ lines.push([word]);
+ }
+ });
+ return lines.map((line) => ` ${line.join(' ')}`).join('\n');
+ }
+
+ /**
+ * @private
+ *
+ * This utilizes `yaml`'s `visit` function to transform nodes with a
+ * comment key pattern to actual comments.
+ *
+ * In Objects, a key of '#' will be converted to a comment at the top of a
+ * property. Any key following the pattern `#|<some key>` will be placed
+ * right before `<some key>`.
+ *
+ * In Arrays, any string that starts with # (including the space), will
+ * be converted to a comment at the position it was in.
+ *
+ * @param { Document } doc
+ * @returns { Document }
+ */
+ static transformComments(doc) {
+ const getLevel = (path) => {
+ const { length } = path.filter((x) => isCollection(x));
+ return length ? length - 1 : 0;
+ };
+
+ visit(doc, {
+ Pair(_, pair, path) {
+ const key = pair.key.value;
+ // If the key is = '#', we add the value as a comment to the parent
+ // We can then remove the node.
+ if (key === '#') {
+ Object.assign(path[path.length - 1], {
+ commentBefore: YamlEditorExtension.wrapCommentString(pair.value.value, getLevel(path)),
+ });
+ return visit.REMOVE;
+ }
+ // If the key starts with `#|`, we want to add a comment to the
+ // corresponding property. We can then remove the node.
+ if (key.startsWith('#|')) {
+ const targetProperty = key.split('|')[1];
+ const target = findPair(path[path.length - 1].items, targetProperty);
+ if (target) {
+ target.key.commentBefore = YamlEditorExtension.wrapCommentString(
+ pair.value.value,
+ getLevel(path),
+ );
+ }
+ return visit.REMOVE;
+ }
+ return undefined; // If the node is not a comment, do nothing with it
+ },
+ // Sequence is basically an array
+ Seq(_, node, path) {
+ let comment = null;
+ const items = node.items.flatMap((child) => {
+ if (comment) {
+ Object.assign(child, { commentBefore: comment });
+ comment = null;
+ }
+ if (
+ isScalar(child) &&
+ child.value &&
+ child.value.startsWith &&
+ child.value.startsWith('#')
+ ) {
+ const commentValue = child.value.replace(/^#\s?/, '');
+ comment = YamlEditorExtension.wrapCommentString(commentValue, getLevel(path));
+ return [];
+ }
+ return child;
+ });
+ Object.assign(node, { items });
+ // Adding a comment in case the last one is a comment
+ if (comment) {
+ Object.assign(node, { comment });
+ }
+ },
+ });
+ return doc;
+ }
+
+ /**
+ * Get the editor's value parsed as a `Document` as defined by the `yaml`
+ * package
+ * @returns {Document}
+ */
+ getDoc() {
+ return parseDocument(this.getValue());
+ }
+
+ /**
+ * Accepts a `Document` as defined by the `yaml` package and
+ * sets the Editor's value to a stringified version of it.
+ * @param { Document } doc
+ */
+ setDoc(doc) {
+ if (this.options.enableComments) {
+ YamlEditorExtension.transformComments(doc);
+ }
+
+ if (!this.getValue()) {
+ this.setValue(doc.toString());
+ } else {
+ this.updateValue(doc.toString());
+ }
+ }
+
+ /**
+ * Returns the parsed value of the Editor's content as JS.
+ * @returns {*}
+ */
+ getDataModel() {
+ return this.getDoc().toJS();
+ }
+
+ /**
+ * Accepts any JS Object and sets the Editor's value to a stringified version
+ * of that value.
+ *
+ * @param value
+ */
+ setDataModel(value) {
+ this.setDoc(new Document(value));
+ }
+
+ /**
+ * Method to be executed when the Editor's <TextModel> was updated
+ */
+ onUpdate() {
+ if (this.options.highlightPath) {
+ this.highlight(this.options.highlightPath);
+ }
+ }
+
+ /**
+ * Set the editors content to the input without recreating the content model.
+ *
+ * @param blob
+ */
+ updateValue(blob) {
+ // Using applyEdits() instead of setValue() ensures that tokens such as
+ // highlighted lines aren't deleted/recreated which causes a flicker.
+ const model = this.getModel();
+ model.applyEdits([
+ {
+ // A nice improvement would be to replace getFullModelRange() with
+ // a range of the actual diff, avoiding re-formatting the document,
+ // but that's something for a later iteration.
+ range: model.getFullModelRange(),
+ text: blob,
+ },
+ ]);
+ }
+
+ /**
+ * Add a line highlight style to the node specified by the path.
+ *
+ * @param {string|null|false} path A path to a node of the Editor's value,
+ * e.g. `"foo.bar[0]"`. If the value is falsy, this will remove all
+ * highlights.
+ */
+ highlight(path) {
+ if (this.options.highlightPath === path) return;
+ if (!path) {
+ SourceEditorExtension.removeHighlights(this);
+ } else {
+ const res = this.locate(path);
+ SourceEditorExtension.highlightLines(this, res);
+ }
+ this.options.highlightPath = path || null;
+ }
+
+ /**
+ * Return the line numbers of a certain node identified by `path` within
+ * the yaml.
+ *
+ * @param {string} path A path to a node, eg. `foo.bar[0]`
+ * @returns {number[]} Array following the schema `[firstLine, lastLine]`
+ * (both inclusive)
+ *
+ * @throws {Error} Will throw if the path is not found inside the document
+ */
+ locate(path) {
+ if (!path) throw Error(`No path provided.`);
+ const blob = this.getValue();
+ const doc = parseDocument(blob);
+ const pathArray = toPath(path);
+
+ if (!doc.getIn(pathArray)) {
+ throw Error(`The node ${path} could not be found inside the document.`);
+ }
+
+ const parentNode = doc.getIn(pathArray.slice(0, pathArray.length - 1));
+ let startChar;
+ let endChar;
+ if (isMap(parentNode)) {
+ const node = parentNode.items.find(
+ (item) => item.key.value === pathArray[pathArray.length - 1],
+ );
+ [startChar] = node.key.range;
+ [, , endChar] = node.value.range;
+ } else {
+ const node = doc.getIn(pathArray);
+ [startChar, , endChar] = node.range;
+ }
+ const startSlice = blob.slice(0, startChar);
+ const endSlice = blob.slice(0, endChar);
+ const startLine = (startSlice.match(/\n/g) || []).length + 1;
+ const endLine = (endSlice.match(/\n/g) || []).length;
+ return [startLine, endLine];
+ }
+}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 0052bc00406..f0db3e5594b 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -63,9 +63,9 @@
"items": {
"type": "object",
"properties": {
- "if": {
- "type": "string"
- },
+ "if": { "$ref": "#/definitions/if" },
+ "changes": { "$ref": "#/definitions/changes" },
+ "exists": { "$ref": "#/definitions/exists" },
"variables": { "$ref": "#/definitions/variables" },
"when": {
"type": "string",
@@ -497,24 +497,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "if": {
- "type": "string",
- "description": "Expression to evaluate whether additional attributes should be provided to the job"
- },
- "changes": {
- "type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
- "items": {
- "type": "string"
- }
- },
- "exists": {
- "type": "array",
- "description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
- "items": {
- "type": "string"
- }
- },
+ "if": { "$ref": "#/definitions/if" },
+ "changes": { "$ref": "#/definitions/changes" },
+ "exists": { "$ref": "#/definitions/exists" },
"variables": { "$ref": "#/definitions/variables" },
"when": { "$ref": "#/definitions/when" },
"start_in": { "$ref": "#/definitions/start_in" },
@@ -541,6 +526,24 @@
]
}
},
+ "if": {
+ "type": "string",
+ "description": "Expression to evaluate whether additional attributes should be provided to the job"
+ },
+ "changes": {
+ "type": "array",
+ "description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
+ "items": {
+ "type": "string"
+ }
+ },
+ "exists": {
+ "type": "array",
+ "description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
+ "items": {
+ "type": "string"
+ }
+ },
"variables": {
"type": "object",
"description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
@@ -555,7 +558,7 @@
},
"start_in": {
"type": "string",
- "description": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job.",
+ "description": "Used in conjunction with 'when: delayed' to set how long to delay before starting a job. e.g. '5', 5 seconds, 30 minutes, 1 week, etc. Read more: https://docs.gitlab.com/ee/ci/jobs/job_control.html#run-a-job-after-a-delay",
"minLength": 1
},
"allow_failure": {
@@ -939,7 +942,7 @@
"stage": {
"type": "string",
"description": "Define what stage the job will run in.",
- "default": "test"
+ "minLength": 1
},
"only": {
"$ref": "#/definitions/filter",
diff --git a/app/assets/javascripts/editor/source_editor_extension.js b/app/assets/javascripts/editor/source_editor_extension.js
new file mode 100644
index 00000000000..f6bc62a1c09
--- /dev/null
+++ b/app/assets/javascripts/editor/source_editor_extension.js
@@ -0,0 +1,17 @@
+import { EDITOR_EXTENSION_DEFINITION_ERROR } from './constants';
+
+export default class EditorExtension {
+ constructor({ definition, setupOptions } = {}) {
+ if (typeof definition !== 'function') {
+ throw new Error(EDITOR_EXTENSION_DEFINITION_ERROR);
+ }
+ this.name = definition.name; // both class- and fn-based extensions have a name
+ this.setupOptions = setupOptions;
+ // eslint-disable-next-line new-cap
+ this.obj = new definition();
+ }
+
+ get api() {
+ return this.obj.provides?.();
+ }
+}
diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js
new file mode 100644
index 00000000000..e0ca4ea518b
--- /dev/null
+++ b/app/assets/javascripts/editor/source_editor_instance.js
@@ -0,0 +1,271 @@
+/**
+ * @module source_editor_instance
+ */
+
+/**
+ * A Source Editor Extension definition
+ * @typedef {Object} SourceEditorExtensionDefinition
+ * @property {Object} definition
+ * @property {Object} setupOptions
+ */
+
+/**
+ * A Source Editor Extension
+ * @typedef {Object} SourceEditorExtension
+ * @property {Object} obj
+ * @property {string} name
+ * @property {Object} api
+ */
+
+import { isEqual } from 'lodash';
+import { editor as monacoEditor } from 'monaco-editor';
+import { getBlobLanguage } from '~/editor/utils';
+import { logError } from '~/lib/logger';
+import { sprintf } from '~/locale';
+import EditorExtension from './source_editor_extension';
+import {
+ EDITOR_EXTENSION_DEFINITION_TYPE_ERROR,
+ EDITOR_EXTENSION_NAMING_CONFLICT_ERROR,
+ EDITOR_EXTENSION_NO_DEFINITION_ERROR,
+ EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
+ EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
+ EDITOR_EXTENSION_STORE_IS_MISSING_ERROR,
+} from './constants';
+
+const utils = {
+ removeExtFromMethod: (method, extensionName, container) => {
+ if (!container) {
+ return;
+ }
+ if (Object.prototype.hasOwnProperty.call(container, method)) {
+ // eslint-disable-next-line no-param-reassign
+ delete container[method];
+ }
+ },
+
+ getStoredExtension: (extensionsStore, name) => {
+ if (!extensionsStore) {
+ logError(EDITOR_EXTENSION_STORE_IS_MISSING_ERROR);
+ return undefined;
+ }
+ return extensionsStore.get(name);
+ },
+};
+
+/** Class representing a Source Editor Instance */
+export default class EditorInstance {
+ /**
+ * Create a Source Editor Instance
+ * @param {Object} rootInstance - Monaco instance to build on top of
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @returns {Object} - A Proxy returning props/methods from either registered extensions, or Source Editor instance, or underlying Monaco instance
+ */
+ constructor(rootInstance = {}, extensionsStore = new Map()) {
+ /** The methods provided by extensions. */
+ this.methods = {};
+
+ const seInstance = this;
+ const getHandler = {
+ get(target, prop, receiver) {
+ const methodExtension =
+ Object.prototype.hasOwnProperty.call(seInstance.methods, prop) &&
+ seInstance.methods[prop];
+ if (methodExtension) {
+ const extension = extensionsStore.get(methodExtension);
+
+ return (...args) => {
+ return extension.api[prop].call(seInstance, ...args, receiver);
+ };
+ }
+ return Reflect.get(seInstance[prop] ? seInstance : target, prop, receiver);
+ },
+ set(target, prop, value) {
+ Object.assign(seInstance, {
+ [prop]: value,
+ });
+ return true;
+ },
+ };
+ const instProxy = new Proxy(rootInstance, getHandler);
+
+ /**
+ * Main entry point to apply an extension to the instance
+ * @param {SourceEditorExtensionDefinition}
+ */
+ this.use = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.useExtension);
+
+ /**
+ * Main entry point to un-use an extension and remove it from the instance
+ * @param {SourceEditorExtension}
+ */
+ this.unuse = EditorInstance.useUnuse.bind(instProxy, extensionsStore, this.unuseExtension);
+
+ return instProxy;
+ }
+
+ /**
+ * A private dispatcher function for both `use` and `unuse`
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {Function} fn - A function to route to. Either `this.useExtension` or `this.unuseExtension`
+ * @param {SourceEditorExtensionDefinition[]} extensions - The extensions to use/unuse.
+ * @returns {Function}
+ */
+ static useUnuse(extensionsStore, fn, extensions) {
+ if (Array.isArray(extensions)) {
+ /**
+ * We cut short if the Array is empty and let the destination function to throw
+ * Otherwise, we run the destination function on every entry of the Array
+ */
+ return extensions.length
+ ? extensions.map(fn.bind(this, extensionsStore))
+ : fn.call(this, extensionsStore);
+ }
+ return fn.call(this, extensionsStore, extensions);
+ }
+
+ //
+ // REGISTERING NEW EXTENSION
+ //
+
+ /**
+ * Run all registrations when using an extension
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {SourceEditorExtensionDefinition} extension - The extension definition to use.
+ * @returns {EditorExtension|*}
+ */
+ useExtension(extensionsStore, extension = {}) {
+ const { definition } = extension;
+ if (!definition) {
+ throw new Error(EDITOR_EXTENSION_NO_DEFINITION_ERROR);
+ }
+ if (typeof definition !== 'function') {
+ throw new Error(EDITOR_EXTENSION_DEFINITION_TYPE_ERROR);
+ }
+
+ // Existing Extension Path
+ const existingExt = utils.getStoredExtension(extensionsStore, definition.name);
+ if (existingExt) {
+ if (isEqual(extension.setupOptions, existingExt.setupOptions)) {
+ return existingExt;
+ }
+ this.unuseExtension(extensionsStore, existingExt);
+ }
+
+ // New Extension Path
+ const extensionInstance = new EditorExtension(extension);
+ const { setupOptions, obj: extensionObj } = extensionInstance;
+ if (extensionObj.onSetup) {
+ extensionObj.onSetup(setupOptions, this);
+ }
+ if (extensionsStore) {
+ this.registerExtension(extensionInstance, extensionsStore);
+ }
+ this.registerExtensionMethods(extensionInstance);
+ return extensionInstance;
+ }
+
+ /**
+ * Register extension in the global extensions store
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ */
+ registerExtension(extension, extensionsStore) {
+ const { name } = extension;
+ const hasExtensionRegistered =
+ extensionsStore.has(name) &&
+ isEqual(extension.setupOptions, extensionsStore.get(name).setupOptions);
+ if (hasExtensionRegistered) {
+ return;
+ }
+ extensionsStore.set(name, extension);
+ const { obj: extensionObj } = extension;
+ if (extensionObj.onUse) {
+ extensionObj.onUse(this);
+ }
+ }
+
+ /**
+ * Register extension methods in the registry on the instance
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension
+ */
+ registerExtensionMethods(extension) {
+ const { api, name } = extension;
+
+ if (!api) {
+ return;
+ }
+
+ Object.keys(api).forEach((prop) => {
+ if (this[prop]) {
+ logError(sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop }));
+ } else {
+ this.methods[prop] = name;
+ }
+ }, this);
+ }
+
+ //
+ // UNREGISTERING AN EXTENSION
+ //
+
+ /**
+ * Unregister extension with the cleanup
+ * @param {Map} extensionsStore - The global registry for the extension instances
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
+ */
+ unuseExtension(extensionsStore, extension) {
+ if (!extension) {
+ throw new Error(EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR);
+ }
+ const { name } = extension;
+ const existingExt = utils.getStoredExtension(extensionsStore, name);
+ if (!existingExt) {
+ throw new Error(sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name }));
+ }
+ const { obj: extensionObj } = existingExt;
+ if (extensionObj.onBeforeUnuse) {
+ extensionObj.onBeforeUnuse(this);
+ }
+ this.unregisterExtensionMethods(existingExt);
+ if (extensionObj.onUnuse) {
+ extensionObj.onUnuse(this);
+ }
+ }
+
+ /**
+ * Remove all methods associated with this extension from the registry on the instance
+ * @param {SourceEditorExtension} extension - Instance of Source Editor extension to un-use
+ */
+ unregisterExtensionMethods(extension) {
+ const { api, name } = extension;
+ if (!api) {
+ return;
+ }
+ Object.keys(api).forEach((method) => {
+ utils.removeExtFromMethod(method, name, this.methods);
+ });
+ }
+
+ /**
+ * PUBLIC API OF AN INSTANCE
+ */
+
+ /**
+ * Updates model language based on the path
+ * @param {String} path - blob path
+ */
+ updateModelLanguage(path) {
+ const lang = getBlobLanguage(path);
+ const model = this.getModel();
+ // return monacoEditor.setModelLanguage(model, lang);
+ monacoEditor.setModelLanguage(model, lang);
+ }
+
+ /**
+ * Get the methods returned by extensions.
+ * @returns {Array}
+ */
+ get extensionsAPI() {
+ return Object.keys(this.methods);
+ }
+}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 7672151af2a..478e3f6aed9 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -9,6 +9,7 @@ let emojiMap = null;
let validEmojiNames = null;
export const FALLBACK_EMOJI_KEY = 'grey_question';
+// Keep the version in sync with `lib/gitlab/emoji.rb`
export const EMOJI_VERSION = '1';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index 2eb2be351b3..26ec882472b 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
@@ -27,7 +27,7 @@ export default {
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
confirmDeleteMessage() {
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
new file mode 100644
index 00000000000..0615bdef537
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
+import folderQuery from '../graphql/queries/folder.query.graphql';
+
+export default {
+ components: {
+ GlCollapse,
+ GlIcon,
+ GlBadge,
+ GlLink,
+ },
+ props: {
+ nestedEnvironment: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return { visible: false };
+ },
+ apollo: {
+ folder: {
+ query: folderQuery,
+ variables() {
+ return { environment: this.nestedEnvironment.latest };
+ },
+ },
+ },
+ computed: {
+ icons() {
+ return this.visible
+ ? { caret: 'angle-down', folder: 'folder-open' }
+ : { caret: 'angle-right', folder: 'folder-o' };
+ },
+ count() {
+ return this.folder?.availableCount ?? 0;
+ },
+ folderClass() {
+ return { 'gl-font-weight-bold': this.visible };
+ },
+ folderPath() {
+ return this.nestedEnvironment.latest.folderPath;
+ },
+ },
+ methods: {
+ toggleCollapse() {
+ this.visible = !this.visible;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
+ <div class="gl-w-full gl-display-flex gl-align-items-center" @click="toggleCollapse">
+ <gl-icon
+ class="gl-mr-2 gl-fill-current-color gl-text-gray-500"
+ :name="icons.caret"
+ :size="12"
+ />
+ <gl-icon class="gl-mr-2 gl-fill-current-color gl-text-gray-500" :name="icons.folder" />
+ <div class="gl-mr-2 gl-text-gray-500" :class="folderClass">
+ {{ nestedEnvironment.name }}
+ </div>
+ <gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
+ <gl-link v-if="visible" :href="folderPath">{{ s__('Environments|Show all') }}</gl-link>
+ </div>
+ <gl-collapse :visible="visible" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
new file mode 100644
index 00000000000..a5526f9cd71
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environments_app.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
+import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql';
+import EnvironmentFolder from './new_environment_folder.vue';
+
+export default {
+ components: {
+ EnvironmentFolder,
+ GlBadge,
+ GlTab,
+ GlTabs,
+ },
+ apollo: {
+ environmentApp: {
+ query: environmentAppQuery,
+ },
+ },
+ computed: {
+ folders() {
+ return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
+ },
+ availableCount() {
+ return this.environmentApp?.availableCount;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tabs>
+ <gl-tab>
+ <template #title>
+ <span>{{ __('Available') }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ availableCount }}
+ </gl-badge>
+ </template>
+ <environment-folder
+ v-for="folder in folders"
+ :key="folder.name"
+ class="gl-mb-3"
+ :nested-environment="folder"
+ />
+ </gl-tab>
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/rollback_modal_manager.vue b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
index 6aa7d96fdfd..3a8b9ebcb84 100644
--- a/app/assets/javascripts/environments/components/rollback_modal_manager.vue
+++ b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
@@ -15,7 +15,6 @@ export default {
data() {
return {
environment: null,
- retryPath: '',
visible: false,
};
},
@@ -35,9 +34,9 @@ export default {
name: environmentName,
commitShortSha,
commitUrl,
+ retryUrl: retryPath,
isLastDeployment: parseBoolean(isLastDeployment),
};
- this.retryPath = retryPath;
this.visible = true;
});
});
@@ -51,7 +50,5 @@ export default {
v-model="visible"
:environment="environment"
:has-multiple-commits="false"
- :retry-url="retryPath"
/>
- <div v-else></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 f248e9ec079..206381e0b7e 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -8,7 +8,7 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
new file mode 100644
index 00000000000..c734c2fba0c
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -0,0 +1,25 @@
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import environmentApp from './queries/environmentApp.query.graphql';
+import { resolvers } from './resolvers';
+import typeDefs from './typedefs.graphql';
+
+export const apolloProvider = (endpoint) => {
+ const defaultClient = createDefaultClient(resolvers(endpoint), {
+ typeDefs,
+ });
+ const { cache } = defaultClient;
+
+ cache.writeQuery({
+ query: environmentApp,
+ data: {
+ availableCount: 0,
+ environments: [],
+ reviewApp: {},
+ stoppedCount: 0,
+ },
+ });
+ return new VueApollo({
+ defaultClient,
+ });
+};
diff --git a/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql
new file mode 100644
index 00000000000..22dfb8a7a89
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/cancel_auto_stop.mutation.graphql
@@ -0,0 +1,5 @@
+mutation cancelAutoStop($environment: LocalEnvironment) {
+ cancelAutoStop(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql
new file mode 100644
index 00000000000..9bb68857923
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/delete_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteEnvironment($environment: LocalEnvironment) {
+ deleteEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql
new file mode 100644
index 00000000000..3db4dc2b9a5
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/rollback_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation rollbackEnvironment($environment: LocalEnvironment) {
+ rollbackEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql
new file mode 100644
index 00000000000..7eae0ef4ce4
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/stop_environment.mutation.graphql
@@ -0,0 +1,5 @@
+mutation stopEnvironment($environment: LocalEnvironment) {
+ stopEnvironment(environment: $environment) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
index 04ea5cbcaef..936bf49a1ac 100644
--- a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
+++ b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($input: EnvironmentsCanaryIngressUpdateInput!) {
+mutation updateCanaryIngress($input: EnvironmentsCanaryIngressUpdateInput!) {
environmentsCanaryIngressUpdate(input: $input) {
errors
}
diff --git a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql b/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
new file mode 100644
index 00000000000..faa76c0a42c
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql
@@ -0,0 +1,8 @@
+query getEnvironmentApp {
+ environmentApp @client {
+ availableCount
+ environments
+ reviewApp
+ stoppedCount
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
new file mode 100644
index 00000000000..3292c916b2e
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -0,0 +1,7 @@
+query getEnvironmentFolder($environment: NestedLocalEnvironment) {
+ folder(environment: $environment) @client {
+ availableCount
+ environments
+ stoppedCount
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
new file mode 100644
index 00000000000..8322b806370
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -0,0 +1,50 @@
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+const mapNestedEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env, { deep: true }),
+ __typename: 'NestedLocalEnvironment',
+});
+const mapEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env),
+ __typename: 'LocalEnvironment',
+});
+
+export const resolvers = (endpoint) => ({
+ Query: {
+ environmentApp() {
+ return axios.get(endpoint, { params: { nested: true } }).then((res) => ({
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapNestedEnvironment),
+ reviewApp: {
+ ...convertObjectPropsToCamelCase(res.data.review_app),
+ __typename: 'ReviewApp',
+ },
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentApp',
+ }));
+ },
+ folder(_, { environment: { folderPath } }) {
+ return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapEnvironment),
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentFolder',
+ }));
+ },
+ },
+ Mutations: {
+ stopEnvironment(_, { environment: { stopPath } }) {
+ return axios.post(stopPath);
+ },
+ deleteEnvironment(_, { environment: { deletePath } }) {
+ return axios.delete(deletePath);
+ },
+ rollbackEnvironment(_, { environment: { retryUrl } }) {
+ return axios.post(retryUrl);
+ },
+ cancelAutoStop(_, { environment: { autoStopPath } }) {
+ return axios.post(autoStopPath);
+ },
+ },
+});
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
new file mode 100644
index 00000000000..49ea719449e
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -0,0 +1,35 @@
+type LocalEnvironment {
+ id: Int!
+ globalId: ID!
+ name: String!
+ folderPath: String
+ stopPath: String
+ deletePath: String
+ retryUrl: String
+ autoStopPath: String
+}
+
+type NestedLocalEnvironment {
+ name: String!
+ size: Int!
+ latest: LocalEnvironment!
+}
+
+type LocalEnvironmentFolder {
+ environments: [LocalEnvironment!]!
+ availableCount: Int!
+ stoppedCount: Int!
+}
+
+type ReviewApp {
+ canSetupReviewApp: Boolean!
+ allClustersEmpty: Boolean!
+ reviewSnippet: String
+}
+
+type LocalEnvironmentApp {
+ stoppedCount: Int!
+ availableCount: Int!
+ environments: [NestedLocalEnvironment!]!
+ reviewApp: ReviewApp!
+}
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 5e33923d518..3b1d35c1f22 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -9,40 +9,43 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
-export default () => {
- const el = document.getElementById('environments-list-view');
- return new Vue({
- el,
- components: {
- environmentsComponent,
- },
- apolloProvider,
- provide: {
- projectPath: el.dataset.projectPath,
- defaultBranchName: el.dataset.defaultBranchName,
- },
- data() {
- const environmentsData = el.dataset;
+export default (el) => {
+ if (el) {
+ return new Vue({
+ el,
+ components: {
+ environmentsComponent,
+ },
+ apolloProvider,
+ provide: {
+ projectPath: el.dataset.projectPath,
+ defaultBranchName: el.dataset.defaultBranchName,
+ },
+ data() {
+ const environmentsData = el.dataset;
- return {
- endpoint: environmentsData.environmentsDataEndpoint,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- };
- },
- render(createElement) {
- return createElement('environments-component', {
- props: {
- endpoint: this.endpoint,
- newEnvironmentPath: this.newEnvironmentPath,
- helpPagePath: this.helpPagePath,
- canCreateEnvironment: this.canCreateEnvironment,
- },
- });
- },
- });
+ return {
+ endpoint: environmentsData.environmentsDataEndpoint,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-component', {
+ props: {
+ endpoint: this.endpoint,
+ newEnvironmentPath: this.newEnvironmentPath,
+ helpPagePath: this.helpPagePath,
+ canCreateEnvironment: this.canCreateEnvironment,
+ },
+ });
+ },
+ });
+ }
+
+ return null;
};
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 85cff73cc3e..0f9741784d6 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -6,7 +6,7 @@ import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/url_utility';
-import { s__ } from '../../locale';
+import { s__, __ } from '../../locale';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
import container from '../components/container.vue';
@@ -207,13 +207,13 @@ export default {
tabs() {
return [
{
- name: s__('Available'),
+ name: __('Available'),
scope: 'available',
count: this.state.availableCounter,
isActive: this.scope === 'available',
},
{
- name: s__('Stopped'),
+ name: __('Stopped'),
scope: 'stopped',
count: this.state.stoppedCounter,
isActive: this.scope === 'stopped',
diff --git a/app/assets/javascripts/environments/new_index.js b/app/assets/javascripts/environments/new_index.js
new file mode 100644
index 00000000000..dd5c709c75a
--- /dev/null
+++ b/app/assets/javascripts/environments/new_index.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { parseBoolean } from '../lib/utils/common_utils';
+import { apolloProvider } from './graphql/client';
+import EnvironmentsApp from './components/new_environments_app.vue';
+
+Vue.use(VueApollo);
+
+export default (el) => {
+ if (el) {
+ const {
+ canCreateEnvironment,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ projectPath,
+ defaultBranchName,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider: apolloProvider(endpoint),
+ provide: {
+ projectPath,
+ defaultBranchName,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ canCreateEnvironment: parseBoolean(canCreateEnvironment),
+ },
+ render(h) {
+ return h(EnvironmentsApp);
+ },
+ });
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 2b8a31da50f..34d01f21da2 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlTooltip, GlSprintf, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -12,6 +12,7 @@ export default {
},
directives: {
GlTooltip,
+ SafeHtml,
},
props: {
lines: {
@@ -129,9 +130,9 @@ export default {
{{ lineNum(line) }}
</td>
<td
+ v-safe-html="lineCode(line)"
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
- v-html="lineCode(line) /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
</template>
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index 624a04fd7c2..dcb6a8e20a3 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -3,11 +3,24 @@ import { get } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() {
- return get(window, ['gon', 'experiment'], {});
+ // Pull from deprecated window.gon.experiment
+ const experimentsFromGon = get(window, ['gon', 'experiment'], {});
+ // Pull from preferred window.gl.experiments
+ const experimentsFromGl = get(window, ['gl', 'experiments'], {});
+
+ return { ...experimentsFromGon, ...experimentsFromGl };
}
function convertExperimentDataToExperimentContext(experimentData) {
- return { schema: TRACKING_CONTEXT_SCHEMA, data: experimentData };
+ // Bandaid to allow-list only the properties which the current gitlab_experiment context schema suppports.
+ // See TRACKING_CONTEXT_SCHEMA for current version (1-0-0)
+ // https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0
+ const { experiment: experimentName, key, variant, migration_keys } = experimentData;
+
+ return {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: experimentName, key, variant, migration_keys },
+ };
}
export function getExperimentData(experimentName) {
@@ -26,14 +39,14 @@ export function getExperimentVariant(experimentName) {
return getExperimentData(experimentName)?.variant || DEFAULT_VARIANT;
}
-export function experiment(experimentName, variants) {
+export function experiment(experimentName, { use, control, candidate, ...variants }) {
const variant = getExperimentVariant(experimentName);
switch (variant) {
case DEFAULT_VARIANT:
- return variants.use.call();
+ return (use || control).call();
case CANDIDATE_VARIANT:
- return variants.try.call();
+ return (variants.try || candidate).call();
default:
return variants[variant].call();
}
diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
index d86e13ce722..366ee6bb05b 100644
--- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
+++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
@@ -213,6 +213,7 @@ export default {
<div
v-if="hasRotateError"
class="gl-text-red-500 gl-display-flex gl-align-items-center gl-font-weight-normal gl-mb-3"
+ data-testid="rotate-error"
>
<gl-icon name="warning" class="gl-mr-2" />
<span>{{ $options.translations.instanceIdRegenerateError }}</span>
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 741171b185a..1287a7ed746 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -9,6 +9,8 @@ const FLASH_TYPES = {
WARNING: 'warning',
};
+const FLASH_CLOSED_EVENT = 'flashClosed';
+
const getCloseEl = (flashEl) => {
return flashEl.querySelector('.js-close-icon');
};
@@ -26,6 +28,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
() => {
flashEl.remove();
window.dispatchEvent(new Event('resize'));
+ flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
if (document.body.classList.contains('flash-shown'))
document.body.classList.remove('flash-shown');
},
@@ -132,4 +135,5 @@ export {
hideFlash,
removeFlashClickListener,
FLASH_TYPES,
+ FLASH_CLOSED_EVENT,
};
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 5dac315d345..1700437aa84 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
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/require-default-prop */
import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -23,6 +22,7 @@ export default {
matcher: {
type: String,
required: false,
+ default: '',
},
itemId: {
type: Number,
@@ -35,6 +35,7 @@ export default {
namespace: {
type: String,
required: false,
+ default: '',
},
webUrl: {
type: String,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index cb63c86a4fa..69331ff1a06 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
-import { escape as lodashEscape, sortBy, template } from 'lodash';
+import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { s__, __, sprintf } from '~/locale';
@@ -65,6 +65,17 @@ export function membersBeforeSave(members) {
});
}
+export const highlighter = (li, query) => {
+ // override default behaviour to escape dot character
+ // see https://github.com/ichord/At.js/pull/576
+ if (!query) {
+ return li;
+ }
+ const escapedQuery = escapeRegExp(query);
+ const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
+ return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
+};
+
export const defaultAutocompleteConfig = {
emojis: true,
members: true,
@@ -664,16 +675,7 @@ class GfmAutoComplete {
}
return null;
},
- highlighter(li, query) {
- // override default behaviour to escape dot character
- // see https://github.com/ichord/At.js/pull/576
- if (!query) {
- return li;
- }
- const escapedQuery = query.replace(/[.+]/, '\\$&');
- const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
- return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
- },
+ highlighter,
};
}
diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue
new file mode 100644
index 00000000000..1e5be9df019
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/app.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import IncubationBanner from './incubation_banner.vue';
+import ServiceAccounts from './service_accounts.vue';
+
+export default {
+ components: { GlTab, GlTabs, IncubationBanner, ServiceAccounts },
+ props: {
+ serviceAccounts: {
+ type: Array,
+ required: true,
+ },
+ createServiceAccountUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ feedbackUrl(template) {
+ return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner
+ :share-feedback-url="feedbackUrl('general_feedback')"
+ :report-bug-url="feedbackUrl('report_bug')"
+ :feature-request-url="feedbackUrl('feature_request')"
+ />
+ <gl-tabs>
+ <gl-tab :title="__('Configuration')">
+ <service-accounts
+ class="gl-mx-3"
+ :list="serviceAccounts"
+ :create-url="createServiceAccountUrl"
+ :empty-illustration-url="emptyIllustrationUrl"
+ />
+ </gl-tab>
+ <gl-tab :title="__('Deployments')" disabled />
+ <gl-tab :title="__('Services')" disabled />
+ </gl-tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/incubation_banner.vue b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
new file mode 100644
index 00000000000..652b8c1aecb
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+
+export default {
+ components: { GlAlert, GlLink, GlSprintf },
+ props: {
+ shareFeedbackUrl: {
+ required: true,
+ type: String,
+ },
+ reportBugUrl: {
+ required: true,
+ type: String,
+ },
+ featureRequestUrl: {
+ required: true,
+ type: String,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert :dismissible="false" variant="info">
+ {{ __('This is an experimental feature developed by GitLab Incubation Engineering.') }}
+ <gl-sprintf
+ :message="
+ __(
+ 'We invite you to %{featureLinkStart}request a feature%{featureLinkEnd}, %{bugLinkStart}report a bug%{bugLinkEnd} or %{feedbackLinkStart}share feedback%{feedbackLinkEnd}',
+ )
+ "
+ >
+ <template #featureLink="{ content }">
+ <gl-link :href="featureRequestUrl">{{ content }}</gl-link>
+ </template>
+ <template #bugLink="{ content }">
+ <gl-link :href="reportBugUrl">{{ content }}</gl-link>
+ </template>
+ <template #feedbackLink="{ content }">
+ <gl-link :href="shareFeedbackUrl">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts.vue b/app/assets/javascripts/google_cloud/components/service_accounts.vue
new file mode 100644
index 00000000000..b70b25a5dc3
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/service_accounts.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton, GlEmptyState, GlTable },
+ props: {
+ list: {
+ type: Array,
+ required: true,
+ },
+ createUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tableFields: [
+ { key: 'environment', label: __('Environment'), sortable: true },
+ { key: 'gcp_project', label: __('Google Cloud Project'), sortable: true },
+ { key: 'service_account_exists', label: __('Service Account'), sortable: true },
+ { key: 'service_account_key_exists', label: __('Service Account Key'), sortable: true },
+ ],
+ };
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-empty-state
+ v-if="list.length === 0"
+ :title="__('No service accounts')"
+ :description="
+ __('Service Accounts keys authorize GitLab to deploy your Google Cloud project')
+ "
+ :primary-button-link="createUrl"
+ :primary-button-text="__('Create service account')"
+ :svg-path="emptyIllustrationUrl"
+ />
+
+ <div v-else>
+ <h2 class="gl-font-size-h2">{{ __('Service Accounts') }}</h2>
+ <p>{{ __('Service Accounts keys authorize GitLab to deploy your Google Cloud project') }}</p>
+
+ <gl-table :items="list" :fields="tableFields">
+ <template #cell(service_account_exists)="{ value }">
+ {{ value ? '✔' : __('Not found') }}
+ </template>
+ <template #cell(service_account_key_exists)="{ value }">
+ {{ value ? '✔' : __('Not found') }}
+ </template>
+ </gl-table>
+
+ <gl-button :href="createUrl" category="primary" variant="info">
+ {{ __('Create service account') }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js
new file mode 100644
index 00000000000..a156a632e9a
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+
+const elementRenderer = (element, props = {}) => (createElement) =>
+ createElement(element, { props });
+
+export default () => {
+ const root = document.querySelector('#js-google-cloud');
+ const props = JSON.parse(root.getAttribute('data'));
+ return new Vue({ el: root, render: elementRenderer(App, props) });
+};
diff --git a/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql
new file mode 100644
index 00000000000..78a368089a8
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Iteration on Iteration {
+ id
+ title
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index e345fe97281..c5f99a1657e 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -1,9 +1,10 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query usersSearch($search: String!, $fullPath: ID!) {
+query groupUsersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) {
- users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
+ id
+ users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
nodes {
user {
...User
diff --git a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
index 1d9497d65ce..62ce27815c7 100644
--- a/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/project_user_members_search.query.graphql
@@ -1,5 +1,6 @@
query searchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
+ id
projectMembers(search: $search) {
nodes {
user {
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index e18eea33041..d04a49f8b3a 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -1,7 +1,7 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query usersSearch($search: String!, $fullPath: ID!) {
+query projectUsersSearch($search: String!, $fullPath: ID!) {
workspace: project(fullPath: $fullPath) {
users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
nodes {
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 828ddd95ffc..8fb70eb59bd 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -15,6 +15,8 @@ export const isGid = (id) => {
return false;
};
+const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10);
+
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number
@@ -23,8 +25,10 @@ export const isGid = (id) => {
* @param {String} gid GraphQL global ID
* @returns {Number}
*/
-export const getIdFromGraphQLId = (gid = '') =>
- parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
+export const getIdFromGraphQLId = (gid = '') => {
+ const parsedGid = parseGid(gid);
+ return Number.isInteger(parsedGid) ? parsedGid : null;
+};
export const MutationOperationMode = {
Append: 'APPEND',
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index b6a1f41afb5..f255f8a084c 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,6 +1,6 @@
import createFlash from '~/flash';
import { __ } from '~/locale';
-import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
+import { getGroupPathAvailability } from '~/rest_api';
import { slugify } from './lib/utils/text_utility';
export default class Group {
@@ -51,7 +51,7 @@ export default class Group {
const slug = this.groupPaths[0]?.value || slugify(value);
if (!slug) return;
- fetchGroupPathAvailability(slug, this.parentId?.value)
+ getGroupPathAvailability(slug, this.parentId?.value)
.then(({ data }) => data)
.then(({ exists, suggests }) => {
if (exists && suggests.length) {
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index f61d96b3dfd..dcac337c6ef 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -3,13 +3,7 @@ import { GlToggle, GlLoadingIcon, GlTooltip, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import {
- DEBOUNCE_TOGGLE_DELAY,
- ERROR_MESSAGE,
- ENABLED,
- DISABLED,
- ALLOW_OVERRIDE,
-} from '../constants';
+import { DEBOUNCE_TOGGLE_DELAY, ERROR_MESSAGE } from '../constants';
export default {
components: {
@@ -18,21 +12,14 @@ export default {
GlTooltip,
GlAlert,
},
- props: {
- updatePath: {
- type: String,
- required: true,
- },
- sharedRunnersAvailability: {
- type: String,
- required: true,
- },
- parentSharedRunnersAvailability: {
- type: String,
- required: false,
- default: '',
- },
- },
+ inject: [
+ 'updatePath',
+ 'sharedRunnersAvailability',
+ 'parentSharedRunnersAvailability',
+ 'runnerEnabled',
+ 'runnerDisabled',
+ 'runnerAllowOverride',
+ ],
data() {
return {
isLoading: false,
@@ -43,21 +30,21 @@ export default {
},
computed: {
toggleDisabled() {
- return this.parentSharedRunnersAvailability === DISABLED || this.isLoading;
+ return this.parentSharedRunnersAvailability === this.runnerDisabled || this.isLoading;
},
enabledOrDisabledSetting() {
- return this.enabled ? ENABLED : DISABLED;
+ return this.enabled ? this.runnerEnabled : this.runnerDisabled;
},
disabledWithOverrideSetting() {
- return this.allowOverride ? ALLOW_OVERRIDE : DISABLED;
+ return this.allowOverride ? this.runnerAllowOverride : this.runnerDisabled;
},
},
created() {
- if (this.sharedRunnersAvailability !== ENABLED) {
+ if (this.sharedRunnersAvailability !== this.runnerEnabled) {
this.enabled = false;
}
- if (this.sharedRunnersAvailability === ALLOW_OVERRIDE) {
+ if (this.sharedRunnersAvailability === this.runnerAllowOverride) {
this.allowOverride = true;
}
},
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index c7bb851c06b..4067b6b52a3 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -4,8 +4,3 @@ import { __ } from '~/locale';
export const DEBOUNCE_TOGGLE_DELAY = 1000;
export const ERROR_MESSAGE = __('Refresh the page and try again.');
-
-// runner setting options
-export const ENABLED = 'enabled';
-export const DISABLED = 'disabled_and_unoverridable';
-export const ALLOW_OVERRIDE = 'disabled_with_override';
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index 44284204c41..21a2373e2b1 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -4,12 +4,27 @@ import UpdateSharedRunnersForm from './components/shared_runners_form.vue';
export default (containerId = 'update-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
+ const {
+ updatePath,
+ sharedRunnersAvailability,
+ parentSharedRunnersAvailability,
+ runnerEnabled,
+ runnerDisabled,
+ runnerAllowOverride,
+ } = containerEl.dataset;
+
return new Vue({
el: containerEl,
+ provide: {
+ updatePath,
+ sharedRunnersAvailability,
+ parentSharedRunnersAvailability,
+ runnerEnabled,
+ runnerDisabled,
+ runnerAllowOverride,
+ },
render(createElement) {
- return createElement(UpdateSharedRunnersForm, {
- props: containerEl.dataset,
- });
+ return createElement(UpdateSharedRunnersForm);
},
});
};
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
index 9c379d7bf9b..a51edd385dd 100644
--- a/app/assets/javascripts/groups/components/item_caret.vue
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -22,6 +22,6 @@ export default {
<template>
<span class="folder-caret gl-mr-2">
- <gl-icon :size="10" :name="iconClass" use-deprecated-sizes />
+ <gl-icon :size="12" :name="iconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 96cb4f3d495..55ae5501cdb 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
@@ -14,6 +14,7 @@ const scrollPositions = {
export default {
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
components: {
GlButton,
@@ -100,8 +101,8 @@ export default {
<pre ref="buildJobLog" class="build-log mb-0 h-100 mr-3" @scroll="scrollBuildLog">
<code
v-show="!detailJob.isLoading"
+ v-safe-html="jobOutput"
class="bash"
- v-html="jobOutput /* eslint-disable-line vue/no-v-html */"
>
</code>
<div
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index e8541d3a4c3..1c5a00568eb 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -2,7 +2,7 @@
import { GlModal, GlButton } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import createFlash from '~/flash';
-import { __, sprintf, s__ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
@@ -58,7 +58,7 @@ export default {
if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
createFlash({
- message: sprintf(s__('The name "%{name}" is already taken in this directory.'), {
+ message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
fadeTransition: false,
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 907ac496982..e1caf1ba44a 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -87,7 +87,7 @@ export default {
v-if="!latestPipeline"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
- class="mb-auto mt-auto"
+ class="gl-p-5"
/>
<gl-alert
v-else-if="latestPipeline.yamlError"
diff --git a/app/assets/javascripts/ide/components/shared/commit_message_field.vue b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
new file mode 100644
index 00000000000..7fca7429ad7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlIcon, GlPopover } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlPopover,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ onCtrlEnter() {
+ if (!this.isFocused) return;
+ this.$emit('submit');
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ triggers: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highlighter helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset
+ class="gl-rounded-base gl-inset-border-1-gray-400 gl-py-4 gl-px-5"
+ :class="{
+ 'gl-outline-none! gl-focus-ring-border-1-gray-900!': isFocused,
+ }"
+ >
+ <div
+ v-once
+ class="gl-display-flex gl-align-items-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-mb-3"
+ >
+ <div>{{ __('Commit Message') }}</div>
+ <div id="commit-message-popover-container">
+ <span id="commit-message-question" class="gl-gray-700 gl-ml-3">
+ <gl-icon name="question" />
+ </span>
+ <gl-popover
+ target="commit-message-question"
+ container="commit-message-popover-container"
+ v-bind="$options.popoverOptions"
+ />
+ </div>
+ </div>
+ <div class="gl-relative gl-w-full gl-h-13 gl-overflow-hidden">
+ <div class="gl-absolute gl-z-index-1 gl-font-monospace gl-text-transparent">
+ <div
+ data-testid="highlights"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`,
+ }"
+ >
+ <div v-for="(line, index) in allLines" :key="index">
+ <span
+ data-testid="highlights-text"
+ class="gl-white-space-pre-wrap gl-word-break-word"
+ v-text="line.text"
+ >
+ </span
+ ><mark
+ v-show="line.highlightedText"
+ data-testid="highlights-mark"
+ class="gl-px-1 gl-py-0 gl-bg-orange-100 gl-text-transparent gl-white-space-pre-wrap gl-word-break-word"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ ref="textarea"
+ :placeholder="placeholder"
+ :value="text"
+ class="gl-absolute gl-w-full gl-h-full gl-z-index-2 gl-font-monospace p-0 gl-outline-0 gl-bg-transparent gl-border-0"
+ data-qa-selector="ide_commit_message_field"
+ dir="auto"
+ name="commit-message"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ @keydown.ctrl.enter="onCtrlEnter"
+ @keydown.meta.enter="onCtrlEnter"
+ >
+ </textarea>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 4845b667b40..706d98fdb90 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -5,7 +5,7 @@ export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
export const SIDEBAR_INIT_WIDTH = 340;
-export const SIDEBAR_MIN_WIDTH = 340;
+export const SIDEBAR_MIN_WIDTH = 260;
export const SIDEBAR_NAV_WIDTH = 60;
// File view modes
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 5ba910746ca..25d4037bbe5 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -19,7 +19,7 @@ export default {
computed: {
filteredNamespaces() {
return this.namespaces.filter((ns) =>
- ns.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
},
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
index 104c84173fc..e004bc35087 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -1,7 +1,5 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { isFinished, isInvalid, isAvailableForImport } from '../utils';
export default {
components: {
@@ -12,32 +10,17 @@ export default {
GlTooltip,
},
props: {
- group: {
- type: Object,
+ isFinished: {
+ type: Boolean,
required: true,
},
- groupPathRegex: {
- type: RegExp,
+ isAvailableForImport: {
+ type: Boolean,
required: true,
},
- },
- computed: {
- fullLastImportPath() {
- return this.group.last_import_target
- ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
- : null;
- },
- absoluteLastImportPath() {
- return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
- },
- isAvailableForImport() {
- return isAvailableForImport(this.group);
- },
- isFinished() {
- return isFinished(this.group);
- },
- isInvalid() {
- return isInvalid(this.group, this.groupPathRegex);
+ isInvalid: {
+ type: Boolean,
+ required: true,
},
},
};
@@ -56,7 +39,7 @@ export default {
{{ isFinished ? __('Re-import') : __('Import') }}
</gl-button>
<gl-icon
- v-if="isFinished"
+ v-if="isAvailableForImport && isFinished"
v-gl-tooltip
:size="16"
name="information-o"
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
index 2de9bd4f868..cad1b983d61 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
@@ -1,7 +1,6 @@
<script>
import { GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
-import { isFinished } from '../utils';
export default {
components: {
@@ -17,16 +16,13 @@ export default {
},
computed: {
fullLastImportPath() {
- return this.group.last_import_target
- ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
+ return this.group.lastImportTarget
+ ? `${this.group.lastImportTarget.targetNamespace}/${this.group.lastImportTarget.newName}`
: null;
},
absoluteLastImportPath() {
return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
},
- isFinished() {
- return isFinished(this.group);
- },
},
};
</script>
@@ -34,13 +30,13 @@ export default {
<template>
<div>
<gl-link
- :href="group.web_url"
+ :href="group.webUrl"
target="_blank"
class="gl-display-inline-flex gl-align-items-center gl-h-7"
>
- {{ group.full_path }} <gl-icon name="external-link" />
+ {{ group.fullPath }} <gl-icon name="external-link" />
</gl-link>
- <div v-if="isFinished && fullLastImportPath" class="gl-font-sm">
+ <div v-if="group.flags.isFinished && fullLastImportPath" class="gl-font-sm">
<gl-sprintf :message="s__('BulkImport|Last imported to %{link}')">
<template #link>
<gl-link :href="absoluteLastImportPath" class="gl-font-sm" target="_blank">{{
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 04b037ecc2b..ec6025c84bb 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -12,18 +12,28 @@ import {
GlTable,
GlFormCheckbox,
} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import createFlash from '~/flash';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import { getGroupPathAvailability } from '~/rest_api';
+import axios from '~/lib/utils/axios_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import { STATUSES } from '../../constants';
import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
-import setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql';
+import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import { isInvalid, isFinished, isAvailableForImport } from '../utils';
+import { NEW_NAME_FIELD, i18n } from '../constants';
+import { StatusPoller } from '../services/status_poller';
+import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils';
import ImportActionsCell from './import_actions_cell.vue';
import ImportSourceCell from './import_source_cell.vue';
import ImportTargetCell from './import_target_cell.vue';
+const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
const DEFAULT_TH_CLASSES =
@@ -59,7 +69,7 @@ export default {
type: RegExp,
required: true,
},
- groupUrlErrorMessage: {
+ jobsPath: {
type: String,
required: true,
},
@@ -70,7 +80,9 @@ export default {
filter: '',
page: 1,
perPage: DEFAULT_PAGE_SIZE,
- selectedGroups: [],
+ selectedGroupsIds: [],
+ pendingGroupsIds: [],
+ importTargets: {},
};
},
@@ -94,14 +106,14 @@ export default {
tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
},
{
- key: 'web_url',
+ key: 'webUrl',
label: s__('BulkImport|From source group'),
thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
},
{
- key: 'import_target',
+ key: 'importTarget',
label: s__('BulkImport|To new group'),
thClass: `${DEFAULT_TH_CLASSES} import-jobs-to-col`,
tdClass: DEFAULT_TD_CLASSES,
@@ -126,16 +138,39 @@ export default {
return this.bulkImportSourceGroups?.nodes ?? [];
},
+ groupsTableData() {
+ return this.groups.map((group) => {
+ const importTarget = this.getImportTarget(group);
+ const status = this.getStatus(group);
+
+ const flags = {
+ isInvalid: importTarget.validationErrors?.length > 0,
+ isAvailableForImport: isAvailableForImport(group) && status !== STATUSES.SCHEDULING,
+ isFinished: isFinished(group),
+ };
+
+ return {
+ ...group,
+ visibleStatus: status,
+ importTarget,
+ flags: {
+ ...flags,
+ isUnselectable: !flags.isAvailableForImport || flags.isInvalid,
+ },
+ };
+ });
+ },
+
hasSelectedGroups() {
- return this.selectedGroups.length > 0;
+ return this.selectedGroupsIds.length > 0;
},
hasAllAvailableGroupsSelected() {
- return this.selectedGroups.length === this.availableGroupsForImport.length;
+ return this.selectedGroupsIds.length === this.availableGroupsForImport.length;
},
availableGroupsForImport() {
- return this.groups.filter((g) => isAvailableForImport(g) && !this.isInvalid(g));
+ return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && g.flags.isInvalid);
},
humanizedTotal() {
@@ -175,25 +210,43 @@ export default {
filter() {
this.page = 1;
},
- groups() {
+
+ groupsTableData() {
const table = this.getTableRef();
- this.groups.forEach((g, idx) => {
- if (this.selectedGroups.includes(g)) {
+ const matches = new Set();
+ this.groupsTableData.forEach((g, idx) => {
+ if (this.selectedGroupsIds.includes(g.id)) {
+ matches.add(g.id);
this.$nextTick(() => {
table.selectRow(idx);
});
}
});
- this.selectedGroups = [];
+
+ this.selectedGroupsIds = this.selectedGroupsIds.filter((id) => matches.has(id));
},
},
- methods: {
- isUnselectable(group) {
- return !this.isAvailableForImport(group) || this.isInvalid(group);
- },
+ mounted() {
+ this.statusPoller = new StatusPoller({
+ pollPath: this.jobsPath,
+ updateImportStatus: (update) => {
+ this.$apollo.mutate({
+ mutation: updateImportStatusMutation,
+ variables: { id: update.id, status: update.status_name },
+ });
+ },
+ });
- rowClasses(group) {
+ this.statusPoller.startPolling();
+ },
+
+ beforeDestroy() {
+ this.statusPoller.stopPolling();
+ },
+
+ methods: {
+ rowClasses(groupTableItem) {
const DEFAULT_CLASSES = [
'gl-border-gray-200',
'gl-border-0',
@@ -201,7 +254,7 @@ export default {
'gl-border-solid',
];
const result = [...DEFAULT_CLASSES];
- if (this.isUnselectable(group)) {
+ if (groupTableItem.flags.isUnselectable) {
result.push('gl-cursor-default!');
}
return result;
@@ -211,19 +264,13 @@ export default {
if (type === 'row') {
return {
'data-qa-selector': 'import_item',
- 'data-qa-source-group': group.full_path,
+ 'data-qa-source-group': group.fullPath,
};
}
return {};
},
- isAvailableForImport,
- isFinished,
- isInvalid(group) {
- return isInvalid(group, this.groupPathRegex);
- },
-
groupsCount(count) {
return n__('%d group', '%d groups', count);
},
@@ -232,22 +279,64 @@ export default {
this.page = page;
},
- updateImportTarget(sourceGroupId, targetNamespace, newName) {
- this.$apollo.mutate({
- mutation: setImportTargetMutation,
- variables: { sourceGroupId, targetNamespace, newName },
- });
+ getStatus(group) {
+ if (this.pendingGroupsIds.includes(group.id)) {
+ return STATUSES.SCHEDULING;
+ }
+
+ return group.progress?.status || STATUSES.NONE;
},
- importGroups(sourceGroupIds) {
- this.$apollo.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds },
+ updateImportTarget(group, changes) {
+ const newImportTarget = {
+ ...group.importTarget,
+ ...changes,
+ };
+ this.$set(this.importTargets, group.id, newImportTarget);
+ this.validateImportTarget(newImportTarget);
+ },
+
+ async importGroups(importRequests) {
+ const newPendingGroupsIds = importRequests.map((request) => request.sourceGroupId);
+ newPendingGroupsIds.forEach((id) => {
+ this.importTargets[id].validationErrors = [
+ { field: NEW_NAME_FIELD, message: i18n.ERROR_IMPORT_COMPLETED },
+ ];
+
+ if (!this.pendingGroupsIds.includes(id)) {
+ this.pendingGroupsIds.push(id);
+ }
});
+
+ try {
+ await this.$apollo.mutate({
+ mutation: importGroupsMutation,
+ variables: { importRequests },
+ });
+ } catch (error) {
+ const message = error?.networkError?.response?.data?.error ?? i18n.ERROR_IMPORT;
+ createFlash({
+ message,
+ captureError: true,
+ error,
+ });
+ } finally {
+ this.pendingGroupsIds = this.pendingGroupsIds.filter(
+ (id) => !newPendingGroupsIds.includes(id),
+ );
+ }
},
importSelectedGroups() {
- this.importGroups(this.selectedGroups.map((g) => g.id));
+ const importRequests = this.groupsTableData
+ .filter((group) => this.selectedGroupsIds.includes(group.id))
+ .map((group) => ({
+ sourceGroupId: group.id,
+ targetNamespace: group.importTarget.targetNamespace.fullPath,
+ newName: group.importTarget.newName,
+ }));
+
+ this.importGroups(importRequests);
},
setPageSize(size) {
@@ -263,16 +352,115 @@ export default {
preventSelectingAlreadyImportedGroups(updatedSelection) {
if (updatedSelection) {
- this.selectedGroups = updatedSelection;
+ this.selectedGroupsIds = updatedSelection.map((g) => g.id);
}
const table = this.getTableRef();
- this.groups.forEach((group, idx) => {
- if (table.isRowSelected(idx) && this.isUnselectable(group)) {
+ this.groupsTableData.forEach((group, idx) => {
+ if (table.isRowSelected(idx) && group.flags.isUnselectable) {
table.unselectRow(idx);
}
});
},
+
+ validateImportTarget: debounce(async function validate(importTarget) {
+ const newValidationErrors = [];
+ importTarget.cancellationToken?.cancel();
+ if (importTarget.newName === '') {
+ newValidationErrors.push({ field: NEW_NAME_FIELD, message: i18n.ERROR_REQUIRED });
+ } else if (!isNameValid(importTarget, this.groupPathRegex)) {
+ newValidationErrors.push({ field: NEW_NAME_FIELD, message: i18n.ERROR_INVALID_FORMAT });
+ } else if (Object.values(this.importTargets).find(isSameTarget(importTarget))) {
+ newValidationErrors.push({
+ field: NEW_NAME_FIELD,
+ message: i18n.ERROR_NAME_ALREADY_USED_IN_SUGGESTION,
+ });
+ } else {
+ try {
+ // eslint-disable-next-line no-param-reassign
+ importTarget.cancellationToken = axios.CancelToken.source();
+ const {
+ data: { exists },
+ } = await getGroupPathAvailability(
+ importTarget.newName,
+ importTarget.targetNamespace.id,
+ {
+ cancelToken: importTarget.cancellationToken?.token,
+ },
+ );
+
+ if (exists) {
+ newValidationErrors.push({
+ field: NEW_NAME_FIELD,
+ message: i18n.ERROR_NAME_ALREADY_EXISTS,
+ });
+ }
+ } catch (e) {
+ if (!axios.isCancel(e)) {
+ throw e;
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ importTarget.validationErrors = newValidationErrors;
+ }, VALIDATION_DEBOUNCE_TIME),
+
+ getImportTarget(group) {
+ if (this.importTargets[group.id]) {
+ return this.importTargets[group.id];
+ }
+
+ const defaultTargetNamespace = this.availableNamespaces[0] ?? { fullPath: '', id: null };
+ let importTarget;
+ if (group.lastImportTarget) {
+ const targetNamespace = this.availableNamespaces.find(
+ (ns) => ns.fullPath === group.lastImportTarget.targetNamespace,
+ );
+
+ importTarget = {
+ targetNamespace: targetNamespace ?? defaultTargetNamespace,
+ newName: group.lastImportTarget.newName,
+ };
+ } else {
+ importTarget = {
+ targetNamespace: defaultTargetNamespace,
+ newName: group.fullPath,
+ };
+ }
+
+ const cancellationToken = axios.CancelToken.source();
+ this.$set(this.importTargets, group.id, {
+ ...importTarget,
+ cancellationToken,
+ validationErrors: [],
+ });
+
+ getGroupPathAvailability(importTarget.newName, importTarget.targetNamespace.id, {
+ cancelToken: cancellationToken.token,
+ })
+ .then(({ data: { exists, suggests: suggestions } }) => {
+ if (!exists) return;
+
+ let currentSuggestion = suggestions[0] ?? importTarget.newName;
+ const existingTargets = Object.values(this.importTargets)
+ .filter((t) => t.targetNamespace.id === importTarget.targetNamespace.id)
+ .map((t) => t.newName.toLowerCase());
+
+ while (existingTargets.includes(currentSuggestion.toLowerCase())) {
+ currentSuggestion = `${currentSuggestion}-1`;
+ }
+
+ Object.assign(this.importTargets[group.id], {
+ targetNamespace: importTarget.targetNamespace,
+ newName: currentSuggestion,
+ });
+ })
+ .catch(() => {
+ // empty catch intended
+ });
+ return this.importTargets[group.id];
+ },
},
gitlabLogo: window.gon.gitlab_logo,
@@ -329,7 +517,7 @@ export default {
<gl-empty-state
v-else-if="!hasGroups"
:title="s__('BulkImport|You have no groups to import')"
- :description="s__('Check your source instance permissions.')"
+ :description="__('Check your source instance permissions.')"
/>
<template v-else>
<div
@@ -337,7 +525,7 @@ export default {
>
<gl-sprintf :message="__('%{count} selected')">
<template #count>
- {{ selectedGroups.length }}
+ {{ selectedGroupsIds.length }}
</template>
</gl-sprintf>
<gl-button
@@ -355,7 +543,7 @@ export default {
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
- :items="groups"
+ :items="groupsTableData"
:fields="$options.fields"
selectable
select-mode="multi"
@@ -364,7 +552,7 @@ export default {
>
<template #head(selected)="{ selectAllRows, clearSelected }">
<gl-form-checkbox
- :key="`checkbox-${selectedGroups.length}`"
+ :key="`checkbox-${selectedGroupsIds.length}`"
class="gl-h-7 gl-pt-3"
:checked="hasSelectedGroups"
:indeterminate="hasSelectedGroups && !hasAllAvailableGroupsSelected"
@@ -375,35 +563,39 @@ export default {
<gl-form-checkbox
class="gl-h-7 gl-pt-3"
:checked="rowSelected"
- :disabled="!isAvailableForImport(group) || isInvalid(group)"
+ :disabled="group.flags.isUnselectable"
@change="rowSelected ? unselectRow() : selectRow()"
/>
</template>
- <template #cell(web_url)="{ item: group }">
+ <template #cell(webUrl)="{ item: group }">
<import-source-cell :group="group" />
</template>
- <template #cell(import_target)="{ item: group }">
+ <template #cell(importTarget)="{ item: group }">
<import-target-cell
:group="group"
:available-namespaces="availableNamespaces"
:group-path-regex="groupPathRegex"
- :group-url-error-message="groupUrlErrorMessage"
- @update-target-namespace="
- updateImportTarget(group.id, $event, group.import_target.new_name)
- "
- @update-new-name="
- updateImportTarget(group.id, group.import_target.target_namespace, $event)
- "
+ @update-target-namespace="updateImportTarget(group, { targetNamespace: $event })"
+ @update-new-name="updateImportTarget(group, { newName: $event })"
/>
</template>
- <template #cell(progress)="{ value: { status } }">
- <import-status-cell :status="status" class="gl-line-height-32" />
+ <template #cell(progress)="{ item: group }">
+ <import-status-cell :status="group.visibleStatus" class="gl-line-height-32" />
</template>
<template #cell(actions)="{ item: group }">
<import-actions-cell
- :group="group"
- :group-path-regex="groupPathRegex"
- @import-group="importGroups([group.id])"
+ :is-finished="group.flags.isFinished"
+ :is-available-for-import="group.flags.isAvailableForImport"
+ :is-invalid="group.flags.isInvalid"
+ @import-group="
+ importGroups([
+ {
+ sourceGroupId: group.id,
+ targetNamespace: group.importTarget.targetNamespace.fullPath,
+ newName: group.importTarget.newName,
+ },
+ ])
+ "
/>
</template>
</gl-table>
@@ -413,7 +605,7 @@ export default {
:page-info="bulkImportSourceGroups.pageInfo"
class="gl-m-0"
/>
- <gl-dropdown category="tertiary" class="gl-ml-auto">
+ <gl-dropdown category="tertiary" :aria-label="__('Page size')" class="gl-ml-auto">
<template #button-content>
<span class="font-weight-bold">
<gl-sprintf :message="__('%{count} items per page')">
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index daced740c94..ca9ae9447d0 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -7,12 +7,7 @@ import {
} from '@gitlab/ui';
import { s__ } from '~/locale';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
-import {
- isInvalid,
- getInvalidNameValidationMessage,
- isNameValid,
- isAvailableForImport,
-} from '../utils';
+import { getInvalidNameValidationMessage } from '../utils';
export default {
components: {
@@ -31,44 +26,15 @@ export default {
type: Array,
required: true,
},
- groupPathRegex: {
- type: RegExp,
- required: true,
- },
- groupUrlErrorMessage: {
- type: String,
- required: true,
- },
},
computed: {
- availableNamespaceNames() {
- return this.availableNamespaces.map((ns) => ns.full_path);
- },
-
- importTarget() {
- return this.group.import_target;
+ fullPath() {
+ return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
},
-
invalidNameValidationMessage() {
- return getInvalidNameValidationMessage(this.group);
+ return getInvalidNameValidationMessage(this.group.importTarget);
},
-
- isInvalid() {
- return isInvalid(this.group, this.groupPathRegex);
- },
-
- isNameValid() {
- return isNameValid(this.group, this.groupPathRegex);
- },
-
- isAvailableForImport() {
- return isAvailableForImport(this.group);
- },
- },
-
- i18n: {
- NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
},
};
</script>
@@ -77,14 +43,14 @@ export default {
<div class="gl-display-flex gl-align-items-stretch">
<import-group-dropdown
#default="{ namespaces }"
- :text="importTarget.target_namespace"
- :disabled="!isAvailableForImport"
- :namespaces="availableNamespaceNames"
+ :text="fullPath"
+ :disabled="!group.flags.isAvailableForImport"
+ :namespaces="availableNamespaces"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
>
- <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
+ <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
s__('BulkImport|No parent')
}}</gl-dropdown-item>
<template v-if="namespaces.length">
@@ -94,20 +60,20 @@ export default {
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
- :key="ns"
+ :key="ns.fullPath"
data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns"
+ :data-qa-group-name="ns.fullPath"
@click="$emit('update-target-namespace', ns)"
>
- {{ ns }}
+ {{ ns.fullPath }}
</gl-dropdown-item>
</template>
</import-group-dropdown>
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
- 'gl-text-gray-400 gl-border-gray-100': !isAvailableForImport,
- 'gl-border-gray-200': isAvailableForImport,
+ 'gl-text-gray-400 gl-border-gray-100': !group.flags.isAvailableForImport,
+ 'gl-border-gray-200': group.flags.isAvailableForImport,
}"
>
/
@@ -116,21 +82,21 @@ export default {
<gl-form-input
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:class="{
- 'gl-inset-border-1-gray-200!': isAvailableForImport,
- 'gl-inset-border-1-gray-100!': !isAvailableForImport,
- 'is-invalid': isInvalid && isAvailableForImport,
+ 'gl-inset-border-1-gray-200!': group.flags.isAvailableForImport,
+ 'gl-inset-border-1-gray-100!': !group.flags.isAvailableForImport,
+ 'is-invalid': group.flags.isInvalid && group.flags.isAvailableForImport,
}"
- :disabled="!isAvailableForImport"
- :value="importTarget.new_name"
+ debounce="500"
+ :disabled="!group.flags.isAvailableForImport"
+ :value="group.importTarget.newName"
+ :aria-label="__('New name')"
@input="$emit('update-new-name', $event)"
/>
- <p v-if="isInvalid" class="gl-text-red-500 gl-m-0 gl-mt-2">
- <template v-if="!isNameValid">
- {{ groupUrlErrorMessage }}
- </template>
- <template v-else-if="invalidNameValidationMessage">
- {{ invalidNameValidationMessage }}
- </template>
+ <p
+ v-if="group.flags.isAvailableForImport && group.flags.isInvalid"
+ class="gl-text-red-500 gl-m-0 gl-mt-2"
+ >
+ {{ invalidNameValidationMessage }}
</p>
</div>
</div>
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
index b2c3d85e280..aa9cf3897e6 100644
--- a/app/assets/javascripts/import_entities/import_groups/constants.js
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -1,7 +1,16 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const i18n = {
- NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+ ERROR_INVALID_FORMAT: s__(
+ 'GroupSettings|Please choose a group URL with no special characters or spaces.',
+ ),
+ ERROR_NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+ ERROR_REQUIRED: __('This field is required.'),
+ ERROR_NAME_ALREADY_USED_IN_SUGGESTION: s__(
+ 'BulkImport|Name already used as a target for another group.',
+ ),
+ ERROR_IMPORT: s__('BulkImport|Importing the group failed.'),
+ ERROR_IMPORT_COMPLETED: s__('BulkImport|Import is finished. Pick another name for re-import'),
};
-export const NEW_NAME_FIELD = 'new_name';
+export const NEW_NAME_FIELD = 'newName';
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index c08cf909a00..bce6e7bcb1f 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -1,23 +1,10 @@
-import createFlash from '~/flash';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
-import { i18n, NEW_NAME_FIELD } from '../constants';
-import { isAvailableForImport } from '../utils';
import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
import bulkImportSourceGroupProgressFragment from './fragments/bulk_import_source_group_progress.fragment.graphql';
-import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql';
-import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql';
-import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql';
-import setImportTargetMutation from './mutations/set_import_target.mutation.graphql';
-import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
-import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql';
-import groupAndProjectQuery from './queries/group_and_project.query.graphql';
-import { SourceGroupsManager } from './services/source_groups_manager';
-import { StatusPoller } from './services/status_poller';
+import { LocalStorageCache } from './services/local_storage_cache';
import typeDefs from './typedefs.graphql';
export const clientTypenames = {
@@ -27,221 +14,99 @@ export const clientTypenames = {
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
- BulkImportValidationError: 'ClientBulkImportValidationError',
};
-function makeGroup(data) {
- const result = {
- __typename: clientTypenames.BulkImportSourceGroup,
+function makeLastImportTarget(data) {
+ return {
+ __typename: clientTypenames.BulkImportTarget,
...data,
};
- const NESTED_OBJECT_FIELDS = {
- import_target: clientTypenames.BulkImportTarget,
- last_import_target: clientTypenames.BulkImportTarget,
- progress: clientTypenames.BulkImportProgress,
- };
-
- Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => {
- if (!data[field]) {
- return;
- }
- result[field] = {
- __typename: type,
- ...data[field],
- };
- });
-
- return result;
}
-async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) {
- const {
- data: { existingGroup, existingProject },
- } = await client.query({
- query: groupAndProjectQuery,
- fetchPolicy: 'no-cache',
- variables: {
- fullPath: `${targetNamespace}/${newName}`,
- },
- });
-
- const variables = {
- field: NEW_NAME_FIELD,
- sourceGroupId,
+function makeProgress(data) {
+ return {
+ __typename: clientTypenames.BulkImportProgress,
+ ...data,
};
-
- if (!existingGroup && !existingProject) {
- client.mutate({
- mutation: removeValidationErrorMutation,
- variables,
- });
- } else {
- client.mutate({
- mutation: addValidationErrorMutation,
- variables: {
- ...variables,
- message: i18n.NAME_ALREADY_EXISTS,
- },
- });
- }
}
-const localProgressId = (id) => `not-started-${id}`;
-const nextName = (name) => `${name}-1`;
+function makeGroup(data) {
+ return {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...data,
+ progress: data.progress
+ ? makeProgress({
+ id: `LOCAL-PROGRESS-${data.id}`,
+ ...data.progress,
+ })
+ : null,
+ lastImportTarget: data.lastImportTarget
+ ? makeLastImportTarget({
+ id: data.id,
+ ...data.lastImportTarget,
+ })
+ : null,
+ };
+}
-export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
- const groupsManager = new GroupsManager({
- sourceUrl,
+function getGroupFromCache({ client, id, getCacheKey }) {
+ return client.readFragment({
+ fragment: bulkImportSourceGroupItemFragment,
+ fragmentName: 'BulkImportSourceGroupItem',
+ id: getCacheKey({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id,
+ }),
});
+}
- let statusPoller;
+export function createResolvers({ endpoints }) {
+ const localStorageCache = new LocalStorageCache();
return {
Query: {
- async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) {
- return client.readFragment({
- fragment: bulkImportSourceGroupItemFragment,
- fragmentName: 'BulkImportSourceGroupItem',
- id: getCacheKey({
- __typename: clientTypenames.BulkImportSourceGroup,
- id,
- }),
+ async bulkImportSourceGroups(_, vars) {
+ const { headers, data } = await axios.get(endpoints.status, {
+ params: {
+ page: vars.page,
+ per_page: vars.perPage,
+ filter: vars.filter,
+ },
});
- },
- async bulkImportSourceGroups(_, vars, { client }) {
- if (!statusPoller) {
- statusPoller = new StatusPoller({
- updateImportStatus: ({ id, status_name: status }) =>
- client.mutate({
- mutation: updateImportStatusMutation,
- variables: { id, status },
- }),
- pollPath: endpoints.jobs,
- });
- statusPoller.startPolling();
- }
-
- return Promise.all([
- axios.get(endpoints.status, {
- params: {
- page: vars.page,
- per_page: vars.perPage,
- filter: vars.filter,
- },
- }),
- client.query({ query: availableNamespacesQuery }),
- ]).then(
- ([
- { headers, data },
- {
- data: { availableNamespaces },
- },
- ]) => {
- const pagination = parseIntPagination(normalizeHeaders(headers));
-
- const response = {
- __typename: clientTypenames.BulkImportSourceGroupConnection,
- nodes: data.importable_data.map((group) => {
- const { jobId, importState: cachedImportState } =
- groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {};
-
- const status = cachedImportState?.status ?? STATUSES.NONE;
-
- const importTarget =
- status === STATUSES.FINISHED && cachedImportState.importTarget
- ? {
- target_namespace: cachedImportState.importTarget.target_namespace,
- new_name: nextName(cachedImportState.importTarget.new_name),
- }
- : cachedImportState?.importTarget ?? {
- new_name: group.full_path,
- target_namespace: availableNamespaces[0]?.full_path ?? '',
- };
-
- return makeGroup({
- ...group,
- validation_errors: [],
- progress: {
- id: jobId ?? localProgressId(group.id),
- status,
- },
- import_target: importTarget,
- last_import_target: cachedImportState?.importTarget ?? null,
- });
- }),
- pageInfo: {
- __typename: clientTypenames.BulkImportPageInfo,
- ...pagination,
- },
- };
-
- setTimeout(() => {
- response.nodes.forEach((group) => {
- if (isAvailableForImport(group)) {
- checkImportTargetIsValid({
- client,
- newName: group.import_target.new_name,
- targetNamespace: group.import_target.target_namespace,
- sourceGroupId: group.id,
- });
- }
- });
+ const pagination = parseIntPagination(normalizeHeaders(headers));
+
+ const response = {
+ __typename: clientTypenames.BulkImportSourceGroupConnection,
+ nodes: data.importable_data.map((group) => {
+ return makeGroup({
+ id: group.id,
+ webUrl: group.web_url,
+ fullPath: group.full_path,
+ fullName: group.full_name,
+ ...group,
+ ...localStorageCache.get(group.web_url),
});
-
- return response;
+ }),
+ pageInfo: {
+ __typename: clientTypenames.BulkImportPageInfo,
+ ...pagination,
},
- );
+ };
+ return response;
},
availableNamespaces: () =>
axios.get(endpoints.availableNamespaces).then(({ data }) =>
data.map((namespace) => ({
__typename: clientTypenames.AvailableNamespace,
- ...namespace,
+ id: namespace.id,
+ fullPath: namespace.full_path,
})),
),
},
Mutation: {
- setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) {
- checkImportTargetIsValid({
- client,
- sourceGroupId,
- targetNamespace,
- newName,
- });
-
- return makeGroup({
- id: sourceGroupId,
- import_target: {
- target_namespace: targetNamespace,
- new_name: newName,
- id: sourceGroupId,
- },
- });
- },
-
- async setImportProgress(_, { sourceGroupId, status, jobId, importTarget }) {
- if (jobId) {
- groupsManager.updateImportProgress(jobId, status);
- }
-
- return makeGroup({
- id: sourceGroupId,
- progress: {
- id: jobId ?? localProgressId(sourceGroupId),
- status,
- },
- last_import_target: {
- __typename: clientTypenames.BulkImportTarget,
- ...importTarget,
- },
- });
- },
-
async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
- groupsManager.updateImportProgress(id, newStatus);
-
const progressItem = client.readFragment({
fragment: bulkImportSourceGroupProgressFragment,
fragmentName: 'BulkImportSourceGroupProgress',
@@ -251,133 +116,62 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
}),
});
- const isInProgress = Boolean(progressItem);
- const { status: currentStatus } = progressItem ?? {};
- if (newStatus === STATUSES.FINISHED && isInProgress && currentStatus !== newStatus) {
- const groups = groupsManager.getImportedGroupsByJobId(id);
+ if (!progressItem) return null;
- groups.forEach(async ({ id: groupId, importTarget }) => {
- client.mutate({
- mutation: setImportTargetMutation,
- variables: {
- sourceGroupId: groupId,
- targetNamespace: importTarget.target_namespace,
- newName: nextName(importTarget.new_name),
- },
- });
- });
- }
+ localStorageCache.updateStatusByJobId(id, newStatus);
return {
__typename: clientTypenames.BulkImportProgress,
+ ...progressItem,
id,
status: newStatus,
};
},
- async addValidationError(_, { sourceGroupId, field, message }, { client }) {
- const {
- data: {
- bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
- },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: sourceGroupId },
- });
+ async importGroups(_, { importRequests }, { client, getCacheKey }) {
+ const importOperations = importRequests.map((importRequest) => {
+ const group = getGroupFromCache({
+ client,
+ getCacheKey,
+ id: importRequest.sourceGroupId,
+ });
- return {
- ...group,
- validation_errors: [
- ...validationErrors.filter(({ field: f }) => f !== field),
- {
- __typename: clientTypenames.BulkImportValidationError,
- field,
- message,
- },
- ],
- };
- },
+ return {
+ group,
+ ...importRequest,
+ };
+ });
- async removeValidationError(_, { sourceGroupId, field }, { client }) {
const {
- data: {
- bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
- },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: sourceGroupId },
+ data: { id: jobId },
+ } = await axios.post(endpoints.createBulkImport, {
+ bulk_import: importOperations.map((op) => ({
+ source_type: 'group_entity',
+ source_full_path: op.group.fullPath,
+ destination_namespace: op.targetNamespace,
+ destination_name: op.newName,
+ })),
});
- return {
- ...group,
- validation_errors: validationErrors.filter(({ field: f }) => f !== field),
- };
- },
-
- async importGroups(_, { sourceGroupIds }, { client }) {
- const groups = await Promise.all(
- sourceGroupIds.map((id) =>
- client
- .query({
- query: bulkImportSourceGroupQuery,
- variables: { id },
- })
- .then(({ data }) => data.bulkImportSourceGroup),
- ),
- );
+ return importOperations.map((op) => {
+ const lastImportTarget = {
+ targetNamespace: op.targetNamespace,
+ newName: op.newName,
+ };
- const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) =>
- makeGroup({
- id: sourceGroupId,
- progress: {
- id: localProgressId(sourceGroupId),
- status: STATUSES.SCHEDULING,
- },
- }),
- );
-
- const defaultErrorMessage = s__('BulkImport|Importing the group failed');
- axios
- .post(endpoints.createBulkImport, {
- bulk_import: groups.map((group) => ({
- source_type: 'group_entity',
- source_full_path: group.full_path,
- destination_namespace: group.import_target.target_namespace,
- destination_name: group.import_target.new_name,
- })),
- })
- .then(({ data: { id: jobId } }) => {
- groupsManager.createImportState(jobId, {
- status: STATUSES.CREATED,
- groups,
- });
+ const progress = {
+ id: jobId,
+ status: STATUSES.CREATED,
+ };
- return { status: STATUSES.CREATED, jobId };
- })
- .catch((e) => {
- const message = e?.response?.data?.error ?? defaultErrorMessage;
- createFlash({ message });
- return { status: STATUSES.NONE };
- })
- .then((newStatus) =>
- sourceGroupIds.forEach((sourceGroupId, idx) =>
- client.mutate({
- mutation: setImportProgressMutation,
- variables: { sourceGroupId, ...newStatus, importTarget: groups[idx].import_target },
- }),
- ),
- )
- .catch(() => createFlash({ message: defaultErrorMessage }));
+ localStorageCache.set(op.group.webUrl, { progress, lastImportTarget });
- return GROUPS_BEING_SCHEDULED;
+ return makeGroup({ ...op.group, progress, lastImportTarget });
+ });
},
},
};
}
export const createApolloClient = ({ sourceUrl, endpoints }) =>
- createDefaultClient(
- createResolvers({ sourceUrl, endpoints }),
- { assumeImmutableResults: true },
- typeDefs,
- );
+ createDefaultClient(createResolvers({ sourceUrl, endpoints }), { typeDefs });
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
index 089340b3c48..0d83be7c0e8 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -2,22 +2,15 @@
fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
id
- web_url
- full_path
- full_name
+ webUrl
+ fullPath
+ fullName
+ lastImportTarget {
+ id
+ targetNamespace
+ newName
+ }
progress {
...BulkImportSourceGroupProgress
}
- import_target {
- target_namespace
- new_name
- }
- last_import_target {
- target_namespace
- new_name
- }
- validation_errors {
- field
- message
- }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
deleted file mode 100644
index d95c460c046..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation addValidationError($sourceGroupId: String!, $field: String!, $message: String!) {
- addValidationError(sourceGroupId: $sourceGroupId, field: $field, message: $message) @client {
- id
- validation_errors {
- field
- message
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
index d8e46329e38..75215471d0f 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
@@ -1,6 +1,11 @@
-mutation importGroups($sourceGroupIds: [String!]!) {
- importGroups(sourceGroupIds: $sourceGroupIds) @client {
+mutation importGroups($importRequests: [ImportGroupInput!]!) {
+ importGroups(importRequests: $importRequests) @client {
id
+ lastImportTarget {
+ id
+ targetNamespace
+ newName
+ }
progress {
id
status
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
deleted file mode 100644
index 940bf4dfaac..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation removeValidationError($sourceGroupId: String!, $field: String!) {
- removeValidationError(sourceGroupId: $sourceGroupId, field: $field) @client {
- id
- validation_errors {
- field
- message
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
deleted file mode 100644
index 43301554de3..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
+++ /dev/null
@@ -1,23 +0,0 @@
-mutation setImportProgress(
- $status: String!
- $sourceGroupId: String!
- $jobId: String
- $importTarget: ImportTargetInput!
-) {
- setImportProgress(
- status: $status
- sourceGroupId: $sourceGroupId
- jobId: $jobId
- importTarget: $importTarget
- ) @client {
- id
- progress {
- id
- status
- }
- last_import_target {
- target_namespace
- new_name
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
deleted file mode 100644
index 793b60ee378..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-mutation setImportTarget($newName: String!, $targetNamespace: String!, $sourceGroupId: String!) {
- setImportTarget(
- newName: $newName
- targetNamespace: $targetNamespace
- sourceGroupId: $sourceGroupId
- ) @client {
- id
- import_target {
- new_name
- target_namespace
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
index 5ab9796b50a..b0741dfbe5c 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
@@ -1,6 +1,6 @@
query availableNamespaces {
availableNamespaces @client {
id
- full_path
+ fullPath
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
deleted file mode 100644
index 0aff23af96d..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-#import "../fragments/bulk_import_source_group_item.fragment.graphql"
-
-query bulkImportSourceGroup($id: ID!) {
- bulkImportSourceGroup(id: $id) @client {
- ...BulkImportSourceGroupItem
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
deleted file mode 100644
index d6124f84025..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query groupAndProject($fullPath: ID!) {
- existingGroup: group(fullPath: $fullPath) {
- id
- }
-
- existingProject: project(fullPath: $fullPath) {
- id
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
new file mode 100644
index 00000000000..09bc7b33692
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -0,0 +1,74 @@
+import { debounce, merge } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const OLD_KEY = 'gl-bulk-imports-import-state';
+export const KEY = 'gl-bulk-imports-import-state-v2';
+export const DEBOUNCE_INTERVAL = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
+
+export class LocalStorageCache {
+ constructor({ storage = window.localStorage } = {}) {
+ this.storage = storage;
+ this.cache = this.loadCacheFromStorage();
+ try {
+ // remove old storage data
+ this.storage.removeItem(OLD_KEY);
+ } catch {
+ // empty catch intended
+ }
+
+ // cache for searching data by jobid
+ this.jobsLookupCache = {};
+ }
+
+ loadCacheFromStorage() {
+ try {
+ return JSON.parse(this.storage.getItem(KEY)) ?? {};
+ } catch {
+ return {};
+ }
+ }
+
+ set(webUrl, data) {
+ this.cache[webUrl] = data;
+ this.saveCacheToStorage();
+ // There are changes to jobIds, drop cache
+ this.jobsLookupCache = {};
+ }
+
+ get(webUrl) {
+ return this.cache[webUrl];
+ }
+
+ getCacheKeysByJobId(jobId) {
+ // this is invoked by polling, so we would like to cache results
+ if (!this.jobsLookupCache[jobId]) {
+ this.jobsLookupCache[jobId] = Object.keys(this.cache).filter(
+ (url) => this.cache[url]?.progress.id === jobId,
+ );
+ }
+
+ return this.jobsLookupCache[jobId];
+ }
+
+ updateStatusByJobId(jobId, status) {
+ this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
+ this.set(webUrl, {
+ ...(this.get(webUrl) ?? {}),
+ progress: {
+ id: jobId,
+ status,
+ },
+ }),
+ );
+ this.saveCacheToStorage();
+ }
+
+ saveCacheToStorage = debounce(() => {
+ try {
+ // storage might be changed in other tab so fetch first
+ this.storage.setItem(KEY, JSON.stringify(merge({}, this.loadCacheFromStorage(), this.cache)));
+ } catch {
+ // empty catch intentional: storage might be unavailable or full
+ }
+ }, DEBOUNCE_INTERVAL);
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
deleted file mode 100644
index 7caa37d9ad4..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { debounce, merge } from 'lodash';
-
-export const KEY = 'gl-bulk-imports-import-state';
-export const DEBOUNCE_INTERVAL = 200;
-
-export class SourceGroupsManager {
- constructor({ sourceUrl, storage = window.localStorage }) {
- this.sourceUrl = sourceUrl;
-
- this.storage = storage;
- this.importStates = this.loadImportStatesFromStorage();
- }
-
- loadImportStatesFromStorage() {
- try {
- return Object.fromEntries(
- Object.entries(JSON.parse(this.storage.getItem(KEY)) ?? {}).map(([jobId, config]) => {
- // new format of storage
- if (config.groups) {
- return [jobId, config];
- }
-
- return [
- jobId,
- {
- status: config.status,
- groups: [{ id: config.id, importTarget: config.importTarget }],
- },
- ];
- }),
- );
- } catch {
- return {};
- }
- }
-
- createImportState(importId, jobConfig) {
- this.importStates[importId] = {
- status: jobConfig.status,
- groups: jobConfig.groups.map((g) => ({
- importTarget: { ...g.import_target },
- id: g.id,
- })),
- };
- this.saveImportStatesToStorage();
- }
-
- updateImportProgress(importId, status) {
- const currentState = this.importStates[importId];
- if (!currentState) {
- return;
- }
-
- currentState.status = status;
- this.saveImportStatesToStorage();
- }
-
- getImportedGroupsByJobId(jobId) {
- return this.importStates[jobId]?.groups ?? [];
- }
-
- getImportStateFromStorageByGroupId(groupId) {
- const [jobId, importState] =
- Object.entries(this.importStates)
- .reverse()
- .find(([, state]) => state.groups.some((g) => g.id === groupId)) ?? [];
-
- if (!jobId) {
- return null;
- }
-
- const group = importState.groups.find((g) => g.id === groupId);
- return { jobId, importState: { ...group, status: importState.status } };
- }
-
- saveImportStatesToStorage = debounce(() => {
- try {
- // storage might be changed in other tab so fetch first
- this.storage.setItem(
- KEY,
- JSON.stringify(merge({}, this.loadImportStatesFromStorage(), this.importStates)),
- );
- } catch {
- // empty catch intentional: storage might be unavailable or full
- }
- }, DEBOUNCE_INTERVAL);
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
deleted file mode 100644
index 0297b3d3428..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
-import { s__ } from '~/locale';
-
-export class StatusPoller {
- constructor({ updateImportStatus, pollPath }) {
- this.eTagPoll = new Poll({
- resource: {
- fetchJobs: () => axios.get(pollPath),
- },
- method: 'fetchJobs',
- successCallback: ({ data: statuses }) => {
- statuses.forEach((status) => updateImportStatus(status));
- },
- errorCallback: () =>
- createFlash({
- message: s__('BulkImport|Update of import statuses with realtime changes failed'),
- }),
- });
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.eTagPoll.restart();
- } else {
- this.eTagPoll.stop();
- }
- });
- }
-
- startPolling() {
- this.eTagPoll.makeRequest();
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index 6ef4bbafec0..b8dd79a5000 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -1,11 +1,11 @@
type ClientBulkImportAvailableNamespace {
id: ID!
- full_path: String!
+ fullPath: String!
}
type ClientBulkImportTarget {
- target_namespace: String!
- new_name: String!
+ targetNamespace: String!
+ newName: String!
}
type ClientBulkImportSourceGroupConnection {
@@ -14,7 +14,7 @@ type ClientBulkImportSourceGroupConnection {
}
type ClientBulkImportProgress {
- id: ID
+ id: ID!
status: String!
}
@@ -25,13 +25,11 @@ type ClientBulkImportValidationError {
type ClientBulkImportSourceGroup {
id: ID!
- web_url: String!
- full_path: String!
- full_name: String!
- progress: ClientBulkImportProgress!
- import_target: ClientBulkImportTarget!
- last_import_target: ClientBulkImportTarget
- validation_errors: [ClientBulkImportValidationError!]!
+ webUrl: String!
+ fullPath: String!
+ fullName: String!
+ lastImportTarget: ClientBulkImportTarget
+ progress: ClientBulkImportProgress
}
type ClientBulkImportPageInfo {
@@ -41,8 +39,13 @@ type ClientBulkImportPageInfo {
totalPages: Int!
}
+type ClientBulkImportNamespaceSuggestion {
+ id: ID!
+ exists: Boolean!
+ suggestions: [String!]!
+}
+
extend type Query {
- bulkImportSourceGroup(id: ID!): ClientBulkImportSourceGroup
bulkImportSourceGroups(
page: Int!
perPage: Int!
@@ -51,26 +54,13 @@ extend type Query {
availableNamespaces: [ClientBulkImportAvailableNamespace!]!
}
-input InputTargetInput {
- target_namespace: String!
- new_name: String!
+input ImportRequestInput {
+ sourceGroupId: ID!
+ targetNamespace: String!
+ newName: String!
}
extend type Mutation {
- setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
- setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
- importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]!
- setImportProgress(
- id: ID
- status: String!
- jobId: String
- importTarget: ImportTargetInput!
- ): ClientBulkImportSourceGroup!
- updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
- addValidationError(
- sourceGroupId: ID!
- field: String!
- message: String!
- ): ClientBulkImportSourceGroup!
- removeValidationError(sourceGroupId: ID!, field: String!): ClientBulkImportSourceGroup!
+ importGroups(importRequests: [ImportRequestInput!]!): [ClientBulkImportSourceGroup!]!
+ updateImportStatus(id: ID, status: String!): ClientBulkImportProgress
}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index 07b839c5c82..67a7258d504 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -17,7 +17,6 @@ export function mountImportGroupsApp(mountElement) {
jobsPath,
sourceUrl,
groupPathRegex,
- groupUrlErrorMessage,
} = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
@@ -26,7 +25,6 @@ export function mountImportGroupsApp(mountElement) {
status: statusPath,
availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
- jobs: jobsPath,
},
}),
});
@@ -38,8 +36,8 @@ export function mountImportGroupsApp(mountElement) {
return createElement(ImportTable, {
props: {
sourceUrl,
+ jobsPath,
groupPathRegex: new RegExp(`^(${groupPathRegex})$`),
- groupUrlErrorMessage,
},
});
},
diff --git a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
new file mode 100644
index 00000000000..ba0f2bb947a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
@@ -0,0 +1,39 @@
+import Visibility from 'visibilityjs';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import { s__ } from '~/locale';
+
+export class StatusPoller {
+ constructor({ updateImportStatus, pollPath }) {
+ this.eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(pollPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data: statuses }) => {
+ statuses.forEach((status) => updateImportStatus(status));
+ },
+ errorCallback: () =>
+ createFlash({
+ message: s__('BulkImport|Update of import statuses with realtime changes failed'),
+ }),
+ });
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.eTagPoll.restart();
+ } else {
+ this.eTagPoll.stop();
+ }
+ });
+ }
+
+ startPolling() {
+ this.eTagPoll.makeRequest();
+ }
+
+ stopPolling() {
+ this.eTagPoll.stop();
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/utils.js b/app/assets/javascripts/import_entities/import_groups/utils.js
index a1baeaf39dd..1d0ab75e1cb 100644
--- a/app/assets/javascripts/import_entities/import_groups/utils.js
+++ b/app/assets/javascripts/import_entities/import_groups/utils.js
@@ -1,22 +1,25 @@
import { STATUSES } from '../constants';
import { NEW_NAME_FIELD } from './constants';
-export function isNameValid(group, validationRegex) {
- return validationRegex.test(group.import_target[NEW_NAME_FIELD]);
+export function isNameValid(importTarget, validationRegex) {
+ return validationRegex.test(importTarget[NEW_NAME_FIELD]);
}
-export function getInvalidNameValidationMessage(group) {
- return group.validation_errors.find(({ field }) => field === NEW_NAME_FIELD)?.message;
-}
-
-export function isInvalid(group, validationRegex) {
- return Boolean(!isNameValid(group, validationRegex) || getInvalidNameValidationMessage(group));
+export function getInvalidNameValidationMessage(importTarget) {
+ return importTarget.validationErrors?.find(({ field }) => field === NEW_NAME_FIELD)?.message;
}
export function isFinished(group) {
- return group.progress.status === STATUSES.FINISHED;
+ return [STATUSES.FINISHED, STATUSES.FAILED].includes(group.progress?.status);
}
export function isAvailableForImport(group) {
- return [STATUSES.NONE, STATUSES.FINISHED].some((status) => group.progress.status === status);
+ return !group.progress || isFinished(group);
+}
+
+export function isSameTarget(importTarget) {
+ return (target) =>
+ target !== importTarget &&
+ target.newName.toLowerCase() === importTarget.newName.toLowerCase() &&
+ target.targetNamespace.id === importTarget.targetNamespace.id;
}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 0cd3519bcec..b9f0b5012ac 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -46,10 +46,6 @@ export default {
return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
},
- availableNamespaces() {
- return this.namespaces.map(({ fullPath }) => fullPath);
- },
-
importAllButtonText() {
if (this.isImportingAnyRepo) {
return n__('Importing %d repository', 'Importing %d repositories', this.importingRepoCount);
@@ -167,7 +163,7 @@ export default {
<provider-repo-table-row
:key="repo.importSource.providerLink"
:repo="repo"
- :available-namespaces="availableNamespaces"
+ :available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
/>
</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index a97af5367fb..c3d0ca4ed8c 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -128,17 +128,17 @@ export default {
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
- :key="ns"
+ :key="ns.fullPath"
data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns"
- @click="updateImportTarget({ targetNamespace: ns })"
+ :data-qa-group-name="ns.fullPath"
+ @click="updateImportTarget({ targetNamespace: ns.fullPath })"
>
- {{ ns }}
+ {{ ns.fullPath }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="updateImportTarget({ targetNamespace: ns })">{{
+ <gl-dropdown-item @click="updateImportTarget({ targetNamespace: userNamespace })">{{
userNamespace
}}</gl-dropdown-item>
</import-group-dropdown>
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 4d34daa43ba..37597da3c8e 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -125,6 +125,7 @@ export default {
'authorUsernameQuery',
'assigneeUsernameQuery',
'slaFeatureAvailable',
+ 'canCreateIncident',
],
apollo: {
incidents: {
@@ -230,13 +231,16 @@ export default {
},
emptyStateData() {
const {
- emptyState: { title, emptyClosedTabTitle, description },
+ emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
createIncidentBtnLabel,
} = this.$options.i18n;
if (this.activeClosedTabHasNoIncidents) {
return { title: emptyClosedTabTitle };
}
+ if (!this.canCreateIncident) {
+ return { title, description: cannotCreateIncidentDescription };
+ }
return {
title,
description,
@@ -244,6 +248,9 @@ export default {
btnText: createIncidentBtnLabel,
};
},
+ isHeaderButtonVisible() {
+ return this.canCreateIncident && (!this.isEmpty || this.activeClosedTabHasNoIncidents);
+ },
},
methods: {
hasAssignees(assignees) {
@@ -311,7 +318,7 @@ export default {
>
<template #header-actions>
<gl-button
- v-if="!isEmpty || activeClosedTabHasNoIncidents"
+ v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index b82980b5628..23909ae3b6c 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -11,7 +11,10 @@ export const I18N = {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__(
- 'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
+ 'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below.',
+ ),
+ cannotCreateIncidentDescription: s__(
+ 'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list.',
),
},
};
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index 6e6461cd7a9..1d40f1093a4 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -21,10 +21,11 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable,
+ canCreateIncident,
} = domEl.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
return new Vue({
@@ -44,6 +45,7 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
+ canCreateIncident: parseBoolean(canCreateIncident),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/init_confirm_danger.js b/app/assets/javascripts/init_confirm_danger.js
new file mode 100644
index 00000000000..d3d32c8be54
--- /dev/null
+++ b/app/assets/javascripts/init_confirm_danger.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import { parseBoolean } from './lib/utils/common_utils';
+import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
+
+export default () => {
+ const el = document.querySelector('.js-confirm-danger');
+ if (!el) return null;
+
+ const {
+ removeFormId = null,
+ phrase,
+ buttonText,
+ buttonTestid = null,
+ confirmDangerMessage,
+ disabled = false,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ confirmDangerMessage,
+ },
+ render: (createElement) =>
+ createElement(ConfirmDanger, {
+ props: {
+ phrase,
+ buttonText,
+ buttonTestid,
+ disabled: parseBoolean(disabled),
+ },
+ on: {
+ confirm: () => {
+ if (removeFormId) document.getElementById(removeFormId)?.submit();
+ },
+ },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 8a8d38b295c..d214ee4ded6 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const TEST_INTEGRATION_EVENT = 'testIntegration';
export const SAVE_INTEGRATION_EVENT = 'saveIntegration';
@@ -21,3 +21,9 @@ export const overrideDropdownDescriptions = {
'Integrations|Default settings are inherited from the instance level.',
),
};
+
+export const I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE = s__(
+ 'Integrations|Connection failed. Please check your settings.',
+);
+export const I18N_DEFAULT_ERROR_MESSAGE = __('Something went wrong on our end.');
+export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection successful.');
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index f30298676df..258cd1bf365 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -62,6 +62,14 @@ export default {
required: false,
default: null,
},
+ /**
+ * The label that is displayed inline with the checkbox.
+ */
+ checkboxLabel: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -152,7 +160,7 @@ export default {
<template v-if="isCheckbox">
<input :name="fieldName" type="hidden" :value="model || false" />
<gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
- {{ humanizedTitle }}
+ {{ checkboxLabel || humanizedTitle }}
</gl-form-checkbox>
</template>
<template v-else-if="isSelect">
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 0521e1eeea5..7cbfb35aeaa 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -5,6 +5,7 @@ import {
VALIDATE_INTEGRATION_FORM_EVENT,
GET_JIRA_ISSUE_TYPES_EVENT,
} from '~/integrations/constants';
+import { s__, __ } from '~/locale';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
@@ -94,33 +95,38 @@ export default {
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
},
},
+ i18n: {
+ sectionTitle: s__('JiraService|View Jira issues in GitLab'),
+ sectionDescription: s__(
+ 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
+ ),
+ enableCheckboxLabel: s__('JiraService|Enable Jira issues'),
+ enableCheckboxHelp: s__(
+ 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
+ ),
+ projectKeyLabel: s__('JiraService|Jira project key'),
+ projectKeyPlaceholder: s__('JiraService|For example, AB'),
+ requiredFieldFeedback: __('This field is required.'),
+ issueTrackerConflictWarning: s__(
+ 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
+ ),
+ },
};
</script>
<template>
<div>
- <gl-form-group
- :label="s__('JiraService|View Jira issues in GitLab')"
- label-for="jira-issue-settings"
- >
+ <gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings">
<div id="jira-issue-settings">
<p>
- {{
- s__(
- 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
- )
- }}
+ {{ $options.i18n.sectionDescription }}
</p>
<template v-if="showJiraIssuesIntegration">
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
<gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting">
- {{ s__('JiraService|Enable Jira issues') }}
+ {{ $options.i18n.enableCheckboxLabel }}
<template #help>
- {{
- s__(
- 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
- )
- }}
+ {{ $options.i18n.enableCheckboxHelp }}
</template>
</gl-form-checkbox>
<template v-if="enableJiraIssues">
@@ -152,30 +158,25 @@ export default {
</gl-form-group>
<template v-if="showJiraIssuesIntegration">
<gl-form-group
- :label="s__('JiraService|Jira project key')"
+ :label="$options.i18n.projectKeyLabel"
label-for="service_project_key"
- :invalid-feedback="__('This field is required.')"
+ :invalid-feedback="$options.i18n.requiredFieldFeedback"
:state="validProjectKey"
+ data-testid="project-key-form-group"
>
<gl-form-input
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
- :placeholder="s__('JiraService|For example, AB')"
+ :placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
:disabled="!enableJiraIssues"
:readonly="isInheriting"
/>
</gl-form-group>
- <p v-if="gitlabIssuesEnabled">
- <gl-sprintf
- :message="
- s__(
- 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
- )
- "
- >
+ <p v-if="gitlabIssuesEnabled" data-testid="conflict-warning-text">
+ <gl-sprintf :message="$options.i18n.issueTrackerConflictWarning">
<template #link="{ content }">
<gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index f33364d5545..f519fc87c46 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,5 +1,4 @@
import { delay } from 'lodash';
-import { __, s__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import axios from '../lib/utils/axios_utils';
import initForm from './edit';
@@ -10,6 +9,9 @@ import {
GET_JIRA_ISSUE_TYPES_EVENT,
TOGGLE_INTEGRATION_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+ I18N_SUCCESSFUL_CONNECTION_MESSAGE,
} from './constants';
export default class IntegrationSettingsForm {
@@ -104,11 +106,7 @@ export default class IntegrationSettingsForm {
return this.fetchTestSettings(formData)
.then(
({
- data: {
- issuetypes,
- error,
- message = s__('Integrations|Connection failed. Please check your settings.'),
- },
+ data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
@@ -118,7 +116,7 @@ export default class IntegrationSettingsForm {
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
- .catch(({ message = __('Something went wrong on our end.') }) => {
+ .catch(({ message = I18N_DEFAULT_ERROR_MESSAGE }) => {
dispatch('receiveJiraIssueTypesError', message);
});
}
@@ -140,11 +138,11 @@ export default class IntegrationSettingsForm {
toast(`${data.message} ${data.service_response}`);
} else {
this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
- toast(s__('Integrations|Connection successful.'));
+ toast(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
}
})
.catch(() => {
- toast(__('Something went wrong on our end.'));
+ toast(I18N_DEFAULT_ERROR_MESSAGE);
})
.finally(() => {
this.vue.$store.dispatch('setIsTesting', false);
diff --git a/app/assets/javascripts/invite_members/components/confetti.vue b/app/assets/javascripts/invite_members/components/confetti.vue
new file mode 100644
index 00000000000..2e5744afcd4
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/confetti.vue
@@ -0,0 +1,33 @@
+<script>
+import confetti from 'canvas-confetti';
+
+export default {
+ mounted() {
+ confetti.create(this.$refs.canvas, {
+ resize: true,
+ useWorker: true,
+ disableForReducedMotion: true,
+ });
+
+ this.basicCannon();
+ },
+ methods: {
+ basicCannon() {
+ confetti({
+ particleCount: 100,
+ spread: 70,
+ origin: { y: 0.2 },
+ scalar: 2,
+ shapes: ['square'],
+ colors: ['#FC6D26', '#6B4FBB', '#FDB997'],
+ zIndex: 1045,
+ gravity: 1.5,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <canvas ref="canvas" width="0" height="0"></canvas>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index cd0b413265b..cf4f434a7a8 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlFormGroup,
GlModal,
GlDropdown,
@@ -11,29 +12,34 @@ import {
GlFormInput,
GlFormCheckboxGroup,
} from '@gitlab/ui';
-import { partition, isString, unescape } from 'lodash';
+import { partition, isString, unescape, uniqueId } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import { s__, sprintf } from '~/locale';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import { sprintf } from '~/locale';
import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
+ INVITE_MEMBERS_FOR_TASK,
+ MODAL_LABELS,
} from '../constants';
import eventHub from '../event_hub';
import {
responseMessageFromError,
responseMessageFromSuccess,
} from '../utils/response_message_parser';
+import ModalConfetti from './confetti.vue';
import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue';
export default {
name: 'InviteMembersModal',
components: {
+ GlAlert,
GlFormGroup,
GlDatepicker,
GlLink,
@@ -46,7 +52,9 @@ export default {
GlFormCheckboxGroup,
MembersTokenSelect,
GroupSelect,
+ ModalConfetti,
},
+ inject: ['newProjectPath'],
props: {
id: {
type: String,
@@ -100,36 +108,54 @@ export default {
type: Array,
required: true,
},
+ tasksToBeDoneOptions: {
+ type: Array,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
},
data() {
return {
visible: true,
- modalId: 'invite-members-modal',
+ modalId: uniqueId('invite-members-modal-'),
selectedAccessLevel: this.defaultAccessLevel,
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
selectedAreasOfFocus: [],
+ selectedTasksToBeDone: [],
+ selectedTaskProject: this.projects[0],
groupToBeSharedWith: {},
source: 'unknown',
invalidFeedbackMessage: '',
isLoading: false,
+ mode: 'default',
};
},
computed: {
+ isCelebration() {
+ return this.mode === 'celebrate';
+ },
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
isInviteGroup() {
return this.inviteeType === 'group';
},
+ modalTitle() {
+ return this.$options.labels[this.inviteeType].modal[this.mode].title;
+ },
introText() {
- const inviteTo = this.isProject ? 'toProject' : 'toGroup';
-
- return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
+ return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, {
name: this.name,
});
},
+ inviteTo() {
+ return this.isProject ? 'toProject' : 'toGroup';
+ },
toastOptions() {
return {
onComplete: () => {
@@ -156,7 +182,7 @@ export default {
);
},
areasOfFocusEnabled() {
- return this.areasOfFocusOptions.length !== 0;
+ return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
},
areasOfFocusForPost() {
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
@@ -172,12 +198,40 @@ export default {
return this.$options.labels[this.inviteeType].placeHolder;
},
+ tasksToBeDoneEnabled() {
+ return (
+ getParameterValues('open_modal')[0] === 'invite_members_for_task' &&
+ this.tasksToBeDoneOptions.length
+ );
+ },
+ showTasksToBeDone() {
+ return (
+ this.tasksToBeDoneEnabled &&
+ this.selectedAccessLevel >= INVITE_MEMBERS_FOR_TASK.minimum_access_level
+ );
+ },
+ showTaskProjects() {
+ return !this.isProject && this.selectedTasksToBeDone.length;
+ },
+ tasksToBeDoneForPost() {
+ return this.showTasksToBeDone ? this.selectedTasksToBeDone : [];
+ },
+ tasksProjectForPost() {
+ return this.showTasksToBeDone && this.selectedTasksToBeDone.length
+ ? this.selectedTaskProject.id
+ : '';
+ },
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
});
+
+ if (this.tasksToBeDoneEnabled) {
+ this.openModal({ inviteeType: 'members', source: 'in_product_marketing_email' });
+ this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, INVITE_MEMBERS_FOR_TASK.view);
+ }
},
methods: {
partitionNewUsersToInvite() {
@@ -191,7 +245,8 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
- openModal({ inviteeType, source }) {
+ openModal({ mode = 'default', inviteeType, source }) {
+ this.mode = mode;
this.inviteeType = inviteeType;
this.source = source;
@@ -219,6 +274,12 @@ export default {
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
},
+ trackinviteMembersForTask() {
+ const label = 'selected_tasks_to_be_done';
+ const property = this.selectedTasksToBeDone.join(',');
+ const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property });
+ tracking.event(INVITE_MEMBERS_FOR_TASK.submit);
+ },
resetFields() {
this.isLoading = false;
this.selectedAccessLevel = this.defaultAccessLevel;
@@ -227,10 +288,15 @@ export default {
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
this.selectedAreasOfFocus = [];
+ this.selectedTasksToBeDone = [];
+ [this.selectedTaskProject] = this.projects;
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
},
+ changeSelectedTaskProject(project) {
+ this.selectedTaskProject = project;
+ },
submitShareWithGroup() {
const apiShareWithGroup = this.isProject
? Api.projectShareWithGroup.bind(Api)
@@ -263,6 +329,7 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
this.trackInvite();
+ this.trackinviteMembersForTask();
Promise.all(promises)
.then(this.conditionallyShowToastSuccess)
@@ -275,6 +342,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
+ tasks_to_be_done: this.tasksToBeDoneForPost,
+ tasks_project_id: this.tasksProjectForPost,
};
},
addByUserIdPostData(usersToAddById) {
@@ -284,6 +353,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
+ tasks_to_be_done: this.tasksToBeDoneForPost,
+ tasks_project_id: this.tasksProjectForPost,
};
},
shareWithGroupPostData(groupToBeSharedWith) {
@@ -322,49 +393,7 @@ export default {
return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
},
},
- labels: {
- members: {
- modalTitle: s__('InviteMembersModal|Invite members'),
- searchField: s__('InviteMembersModal|GitLab member or email address'),
- placeHolder: s__('InviteMembersModal|Select members or type email addresses'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- },
- group: {
- modalTitle: s__('InviteMembersModal|Invite a group'),
- searchField: s__('InviteMembersModal|Select a group to invite'),
- placeHolder: s__('InviteMembersModal|Search for a group to invite'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- },
- accessLevel: s__('InviteMembersModal|Select a role'),
- accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
- toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
- invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
- readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
- inviteButtonText: s__('InviteMembersModal|Invite'),
- cancelButtonText: s__('InviteMembersModal|Cancel'),
- headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
- areasOfFocusLabel: s__(
- 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
- ),
- },
+ labels: MODAL_LABELS,
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
@@ -374,20 +403,29 @@ export default {
:modal-id="modalId"
size="sm"
data-qa-selector="invite_members_modal_content"
- :title="$options.labels[inviteeType].modalTitle"
+ data-testid="invite-members-modal"
+ :title="modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
@hidden="resetFields"
@close="resetFields"
@hide="resetFields"
>
<div>
- <p ref="introText">
- <gl-sprintf :message="introText">
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
+ <div class="gl-display-flex">
+ <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div>
+ <div>
+ <p ref="introText">
+ <gl-sprintf :message="introText">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <br />
+ <span v-if="isCelebration">{{ $options.labels.members.modal.celebrate.intro }} </span>
+ <modal-confetti v-if="isCelebration" />
+ </p>
+ </div>
+ </div>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
@@ -476,24 +514,70 @@ export default {
data-testid="area-of-focus-checks"
/>
</div>
+ <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
+ <label class="gl-mt-5">
+ {{ $options.labels.members.tasksToBeDone.title }}
+ </label>
+ <template v-if="projects.length">
+ <gl-form-checkbox-group
+ v-model="selectedTasksToBeDone"
+ :options="tasksToBeDoneOptions"
+ data-testid="invite-members-modal-tasks"
+ />
+ <template v-if="showTaskProjects">
+ <label class="gl-mt-5 gl-display-block">
+ {{ $options.labels.members.tasksProject.title }}
+ </label>
+ <gl-dropdown
+ class="gl-w-half gl-xs-w-full"
+ :text="selectedTaskProject.title"
+ data-testid="invite-members-modal-project-select"
+ >
+ <template v-for="project in projects">
+ <gl-dropdown-item
+ :key="project.id"
+ active-class="is-active"
+ is-check-item
+ :is-checked="project.id === selectedTaskProject.id"
+ @click="changeSelectedTaskProject(project)"
+ >
+ {{ project.title }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ </template>
+ </template>
+ <gl-alert
+ v-else-if="tasksToBeDoneEnabled"
+ variant="tip"
+ :dismissible="false"
+ data-testid="invite-members-modal-no-projects-alert"
+ >
+ <gl-sprintf :message="$options.labels.members.tasksToBeDone.noProjects">
+ <template #link="{ content }">
+ <gl-link :href="newProjectPath" target="_blank" class="gl-label-link">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ </div>
</div>
<template #modal-footer>
- <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
- <gl-button data-testid="cancel-button" @click="closeModal">
- {{ $options.labels.cancelButtonText }}
- </gl-button>
- <div class="gl-mr-3"></div>
- <gl-button
- :disabled="inviteDisabled"
- :loading="isLoading"
- variant="success"
- data-qa-selector="invite_button"
- data-testid="invite-button"
- @click="sendInvite"
- >{{ $options.labels.inviteButtonText }}</gl-button
- >
- </div>
+ <gl-button data-testid="cancel-button" @click="closeModal">
+ {{ $options.labels.cancelButtonText }}
+ </gl-button>
+ <gl-button
+ :disabled="inviteDisabled"
+ :loading="isLoading"
+ variant="success"
+ data-qa-selector="invite_button"
+ data-testid="invite-button"
+ @click="sendInvite"
+ >
+ {{ $options.labels.inviteButtonText }}
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 05be427742c..bf3250f63a5 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '../constants';
export default {
- components: { GlButton, GlLink },
+ components: { GlButton, GlLink, GlIcon },
props: {
displayText: {
type: String,
@@ -53,13 +54,11 @@ export default {
},
},
computed: {
- isButton() {
- return this.triggerElement === 'button';
- },
componentAttributes() {
const baseAttributes = {
class: this.classes,
'data-qa-selector': 'invite_members_button',
+ 'data-test-id': 'invite-members-button',
};
if (this.event && this.label) {
@@ -77,6 +76,9 @@ export default {
this.trackExperimentOnShow();
},
methods: {
+ checkTrigger(targetTriggerElement) {
+ return this.triggerElement === targetTriggerElement;
+ },
openModal() {
eventHub.$emit('openModal', { inviteeType: 'members', source: this.triggerSource });
},
@@ -87,12 +89,14 @@ export default {
}
},
},
+ TRIGGER_ELEMENT_BUTTON,
+ TRIGGER_ELEMENT_SIDE_NAV,
};
</script>
<template>
<gl-button
- v-if="isButton"
+ v-if="checkTrigger($options.TRIGGER_ELEMENT_BUTTON)"
v-bind="componentAttributes"
:variant="variant"
:icon="icon"
@@ -100,6 +104,17 @@ export default {
>
{{ displayText }}
</gl-button>
+ <gl-link
+ v-else-if="checkTrigger($options.TRIGGER_ELEMENT_SIDE_NAV)"
+ v-bind="componentAttributes"
+ data-is-link="true"
+ @click="openModal"
+ >
+ <span class="nav-icon-container">
+ <gl-icon :name="icon" />
+ </span>
+ <span class="nav-item-name"> {{ displayText }} </span>
+ </gl-link>
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
{{ displayText }}
</gl-link>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index d7daf83e26b..59d4c2f3077 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
@@ -8,6 +8,12 @@ export const MEMBER_AREAS_OF_FOCUS = {
view: 'view',
submit: 'submit',
};
+export const INVITE_MEMBERS_FOR_TASK = {
+ minimum_access_level: 30,
+ name: 'invite_members_for_task',
+ view: 'modal_opened_from_email',
+ submit: 'submit',
+};
export const GROUP_FILTERS = {
ALL: 'all',
@@ -19,3 +25,122 @@ export const API_MESSAGES = {
};
export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
+export const TRIGGER_ELEMENT_BUTTON = 'button';
+export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
+export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
+export const MEMBERS_MODAL_CELEBRATE_TITLE = s__(
+ 'InviteMembersModal|GitLab is better with colleagues!',
+);
+export const MEMBERS_MODAL_CELEBRATE_INTRO = s__(
+ 'InviteMembersModal|How about inviting a colleague or two to join you?',
+);
+export const MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
+);
+
+export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
+);
+export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
+ "InviteMembersModal|Congratulations on creating your project, you're almost there!",
+);
+export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
+export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
+export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
+ 'InviteMembersModal|Create issues for your new team member to work on (optional)',
+);
+export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
+ 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
+);
+export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
+ 'InviteMembersModal|Choose a project for the issues',
+);
+
+export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
+export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
+);
+export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
+);
+
+export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
+export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
+
+export const ACCESS_LEVEL = s__('InviteMembersModal|Select a role');
+export const ACCESS_EXPIRE_DATE = s__('InviteMembersModal|Access expiration date (optional)');
+export const TOAST_MESSAGE_SUCCESSFUL = s__('InviteMembersModal|Members were successfully added');
+export const INVALID_FEEDBACK_MESSAGE_DEFAULT = s__('InviteMembersModal|Something went wrong');
+export const READ_MORE_TEXT = s__(
+ `InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
+);
+export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
+export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
+export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
+export const AREAS_OF_FOCUS_LABEL = s__(
+ 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
+);
+
+export const MODAL_LABELS = {
+ members: {
+ modal: {
+ default: {
+ title: MEMBERS_MODAL_DEFAULT_TITLE,
+ },
+ celebrate: {
+ title: MEMBERS_MODAL_CELEBRATE_TITLE,
+ intro: MEMBERS_MODAL_CELEBRATE_INTRO,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ celebrate: {
+ introText: MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
+ },
+ },
+ searchField: MEMBERS_SEARCH_FIELD,
+ placeHolder: MEMBERS_PLACEHOLDER,
+ tasksToBeDone: {
+ title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
+ noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
+ },
+ tasksProject: {
+ title: MEMBERS_TASKS_PROJECTS_TITLE,
+ },
+ },
+ group: {
+ modal: {
+ default: {
+ title: GROUP_MODAL_DEFAULT_TITLE,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ },
+ searchField: GROUP_SEARCH_FIELD,
+ placeHolder: GROUP_PLACEHOLDER,
+ },
+ accessLevel: ACCESS_LEVEL,
+ accessExpireDate: ACCESS_EXPIRE_DATE,
+ toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
+ invalidFeedbackMessageDefault: INVALID_FEEDBACK_MESSAGE_DEFAULT,
+ readMoreText: READ_MORE_TEXT,
+ inviteButtonText: INVITE_BUTTON_TEXT,
+ cancelButtonText: CANCEL_BUTTON_TEXT,
+ headerCloseLabel: HEADER_CLOSE_LABEL,
+ areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
+};
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index c1dfaa25dc7..fc657a064dd 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -5,15 +5,32 @@ import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast);
+let initedInviteMembersModal;
+
export default function initInviteMembersModal() {
+ if (initedInviteMembersModal) {
+ // if we already loaded this in another part of the dom, we don't want to do it again
+ // else we will stack the modals
+ return false;
+ }
+
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/344955
+ // bug lying in wait here for someone to put group and project invite in same screen
+ // once that happens we'll need to mount these differently, perhaps split
+ // group/project to each mount one, with many ways to open it.
const el = document.querySelector('.js-invite-members-modal');
if (!el) {
return false;
}
+ initedInviteMembersModal = true;
+
return new Vue({
el,
+ provide: {
+ newProjectPath: el.dataset.newProjectPath,
+ },
render: (createElement) =>
createElement(InviteMembersModal, {
props: {
@@ -24,6 +41,8 @@ export default function initInviteMembersModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
+ tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
+ projects: JSON.parse(el.dataset.projects || '[]'),
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
index 6e300831e00..799d2bdc9e2 100644
--- a/app/assets/javascripts/issuable/components/issuable_by_email.vue
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -166,7 +166,7 @@ export default {
</gl-sprintf>
</p>
<template #modal-footer>
- <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ <gl-button category="secondary" @click="cancelHandler">{{ __('Cancel') }}</gl-button>
</template>
</gl-modal>
</div>
diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js
index 22a99a17741..8f7f317d6b4 100644
--- a/app/assets/javascripts/issuable_suggestions/index.js
+++ b/app/assets/javascripts/issuable_suggestions/index.js
@@ -10,12 +10,7 @@ export default function initIssuableSuggestions() {
const issueTitle = document.getElementById('issue_title');
const { projectPath } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ 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 2c9a512acdb..d3b58ed3012 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -4,7 +4,7 @@ import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
-import { __, s__, sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import {
IssuableStatus,
IssuableStatusText,
@@ -12,6 +12,7 @@ import {
IssueTypePath,
IncidentTypePath,
IncidentType,
+ POLLING_DELAY,
} from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
@@ -249,7 +250,7 @@ export default {
return false;
},
defaultErrorMessage() {
- return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
+ return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
isClosed() {
return this.issuableStatus === IssuableStatus.Closed;
@@ -282,7 +283,7 @@ export default {
});
if (!Visibility.hidden()) {
- this.poll.makeDelayedRequest(2000);
+ this.poll.makeDelayedRequest(POLLING_DELAY);
}
Visibility.change(() => {
@@ -436,7 +437,7 @@ export default {
})
.catch(() => {
createFlash({
- message: sprintf(s__('Error deleting %{issuableType}'), {
+ message: sprintf(__('Error deleting %{issuableType}'), {
issuableType: this.issuableType,
}),
});
@@ -457,6 +458,22 @@ export default {
this.flashContainer = null;
}
},
+
+ taskListUpdateStarted() {
+ this.poll.stop();
+ },
+
+ taskListUpdateSucceeded() {
+ this.poll.enable();
+ this.poll.makeDelayedRequest(POLLING_DELAY);
+ },
+
+ taskListUpdateFailed() {
+ this.poll.enable();
+ this.poll.makeDelayedRequest(POLLING_DELAY);
+
+ this.updateStoreState();
+ },
},
};
</script>
@@ -552,7 +569,9 @@ export default {
:issuable-type="issuableType"
:update-url="updateEndpoint"
:lock-version="state.lock_version"
- @taskListUpdateFailed="updateStoreState"
+ @taskListUpdateStarted="taskListUpdateStarted"
+ @taskListUpdateSucceeded="taskListUpdateSucceeded"
+ @taskListUpdateFailed="taskListUpdateFailed"
/>
<edited-component
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 4c6a1478e95..9dc122d426c 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -2,7 +2,7 @@
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import createFlash from '~/flash';
-import { s__, sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import TaskList from '../../task_list';
import animateMixin from '../mixins/animate';
@@ -86,15 +86,25 @@ export default {
fieldName: 'description',
lockVersion: this.lockVersion,
selector: '.detail-page-description',
+ onUpdate: this.taskListUpdateStarted.bind(this),
+ onSuccess: this.taskListUpdateSuccess.bind(this),
onError: this.taskListUpdateError.bind(this),
});
}
},
+ taskListUpdateStarted() {
+ this.$emit('taskListUpdateStarted');
+ },
+
+ taskListUpdateSuccess() {
+ this.$emit('taskListUpdateSucceeded');
+ },
+
taskListUpdateError() {
createFlash({
message: 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.',
),
{
diff --git a/app/assets/javascripts/issue_show/components/fields/type.vue b/app/assets/javascripts/issue_show/components/fields/type.vue
index 3eac448c637..9110a6924b4 100644
--- a/app/assets/javascripts/issue_show/components/fields/type.vue
+++ b/app/assets/javascripts/issue_show/components/fields/type.vue
@@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
-import { IssuableTypes } from '../../constants';
+import { IssuableTypes, IncidentType } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
@@ -19,6 +19,14 @@ export default {
GlDropdown,
GlDropdownItem,
},
+ inject: {
+ canCreateIncident: {
+ default: false,
+ },
+ issueType: {
+ default: 'issue',
+ },
+ },
data() {
return {
issueState: {},
@@ -36,6 +44,9 @@ export default {
} = this;
return capitalize(issueType);
},
+ shouldShowIncident() {
+ return this.issueType === IncidentType || this.canCreateIncident;
+ },
},
methods: {
updateIssueType(issueType) {
@@ -47,6 +58,9 @@ export default {
},
});
},
+ isShown(type) {
+ return type.value !== IncidentType || this.shouldShowIncident;
+ },
},
};
</script>
@@ -68,6 +82,7 @@ export default {
>
<gl-dropdown-item
v-for="type in $options.IssuableTypes"
+ v-show="isShown(type)"
:key="type.value"
:is-checked="issueState.issueType === type.value"
is-check-item
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 2bddbe4faa0..2c314ce1c3f 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -192,9 +192,14 @@ export default {
class="gl-sm-display-none! w-100"
block
:text="dropdownText"
+ data-qa-selector="issue_actions_dropdown"
:loading="isToggleStateButtonLoading"
>
- <gl-dropdown-item v-if="showToggleIssueStateButton" @click="toggleIssueState">
+ <gl-dropdown-item
+ v-if="showToggleIssueStateButton"
+ :data-qa-selector="`mobile_${qaSelector}`"
+ @click="toggleIssueState"
+ >
{{ buttonText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js
index 64d39a79821..ef9699deb42 100644
--- a/app/assets/javascripts/issue_show/constants.js
+++ b/app/assets/javascripts/issue_show/constants.js
@@ -37,3 +37,10 @@ export const IncidentTypePath = 'issues/incident';
export const IncidentType = 'incident';
export const issueState = { issueType: undefined, isDirty: false };
+
+export const POLLING_DELAY = 2000;
+
+export const WorkspaceType = {
+ project: 'project',
+ group: 'group',
+};
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
index df986195656..3aff2d9c54a 100644
--- a/app/assets/javascripts/issue_show/incident.js
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -2,25 +2,31 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
-import { issueState } from './constants';
+import { issueState, IncidentType } from './constants';
import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
+import HeaderActions from './components/header_actions.vue';
-export default function initIssuableApp(issuableData = {}) {
+const bootstrapApollo = (state = {}) => {
+ return apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getIssueStateQuery,
+ data: {
+ issueState: state,
+ },
+ });
+};
+
+export function initIncidentApp(issuableData = {}) {
const el = document.getElementById('js-issuable-app');
if (!el) {
return undefined;
}
- apolloProvider.clients.defaultClient.cache.writeQuery({
- query: getIssueStateQuery,
- data: {
- issueState: { ...issueState, issueType: el.dataset.issueType },
- },
- });
+ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const {
+ canCreateIncident,
canUpdate,
iid,
projectNamespace,
@@ -39,6 +45,8 @@ export default function initIssuableApp(issuableData = {}) {
issuableApp,
},
provide: {
+ issueType: IncidentType,
+ canCreateIncident,
canUpdate,
fullPath,
iid,
@@ -57,3 +65,35 @@ export default function initIssuableApp(issuableData = {}) {
},
});
}
+
+export function initIncidentHeaderActions(store) {
+ const el = document.querySelector('.js-issue-header-actions');
+
+ if (!el) {
+ return undefined;
+ }
+
+ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ store,
+ provide: {
+ canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
+ canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
+ canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
+ canReportSpam: parseBoolean(el.dataset.canReportSpam),
+ canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
+ iid: el.dataset.iid,
+ isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
+ issueType: el.dataset.issueType,
+ newIssuePath: el.dataset.newIssuePath,
+ projectPath: el.dataset.projectPath,
+ projectId: el.dataset.projectId,
+ reportAbusePath: el.dataset.reportAbusePath,
+ submitAsSpamPath: el.dataset.submitAsSpamPath,
+ },
+ render: (createElement) => createElement(HeaderActions),
+ });
+}
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js
index 4374dba6eb7..25cc51478ff 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -25,17 +25,22 @@ export function initIssuableApp(issuableData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+ const { canCreateIncident, ...issuableProps } = issuableData;
+
return new Vue({
el,
apolloProvider,
store,
+ provide: {
+ canCreateIncident,
+ },
computed: {
...mapGetters(['getNoteableData']),
},
render(createElement) {
return createElement(IssuableApp, {
props: {
- ...issuableData,
+ ...issuableProps,
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index 7b51f6ee46a..7f2082e5b90 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -36,6 +36,7 @@ import {
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
@@ -65,6 +66,7 @@ import {
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
TOKEN_TITLE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -88,6 +90,8 @@ const LabelToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
+const ReleaseToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
const WeightToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue');
@@ -165,6 +169,9 @@ export default {
newIssuePath: {
default: '',
},
+ releasesPath: {
+ default: '',
+ },
rssPath: {
default: '',
},
@@ -288,6 +295,7 @@ export default {
avatar_url: gon.current_user_avatar_url,
});
}
+
const tokens = [
{
type: TOKEN_TYPE_AUTHOR,
@@ -297,7 +305,6 @@ export default {
dataType: 'user',
unique: true,
defaultAuthors: [],
- operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers,
preloadedAuthors,
},
@@ -317,7 +324,6 @@ export default {
title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
token: MilestoneToken,
- unique: true,
fetchMilestones: this.fetchMilestones,
},
{
@@ -333,7 +339,6 @@ export default {
title: TOKEN_TITLE_TYPE,
icon: 'issues',
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
options: [
{ icon: 'issue-type-issue', title: 'issue', value: 'issue' },
{ icon: 'issue-type-incident', title: 'incident', value: 'incident' },
@@ -342,6 +347,16 @@ export default {
},
];
+ if (this.isProject) {
+ tokens.push({
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
+ icon: 'rocket',
+ token: ReleaseToken,
+ fetchReleases: this.fetchReleases,
+ });
+ }
+
if (this.isSignedIn) {
tokens.push({
type: TOKEN_TYPE_MY_REACTION,
@@ -349,7 +364,6 @@ export default {
icon: 'thumb-up',
token: EmojiToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
fetchEmojis: this.fetchEmojis,
});
@@ -373,7 +387,6 @@ export default {
title: TOKEN_TITLE_ITERATION,
icon: 'iteration',
token: IterationToken,
- unique: true,
fetchIterations: this.fetchIterations,
});
}
@@ -459,6 +472,9 @@ export default {
fetchEmojis(search) {
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
},
+ fetchReleases(search) {
+ return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
+ },
fetchLabels(search) {
return this.$apollo
.query({
diff --git a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
index 037fd9be542..e749579af80 100644
--- a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
@@ -71,8 +71,11 @@ export default {
hasSelectedProject() {
return this.selectedProject.id;
},
+ projectsWithIssuesEnabled() {
+ return this.projects.filter((project) => project.issuesEnabled);
+ },
showNoSearchResultsText() {
- return !this.projects.length && this.search;
+ return !this.projectsWithIssuesEnabled.length && this.search;
},
},
methods: {
@@ -110,7 +113,7 @@ export default {
<gl-loading-icon v-if="$apollo.queries.projects.loading" />
<template v-else>
<gl-dropdown-item
- v-for="project of projects"
+ v-for="project of projectsWithIssuesEnabled"
:key="project.id"
@click="selectProject(project)"
>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 5bdc1bd9f90..da9b96d0e22 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -166,6 +166,7 @@ const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
+const TITLE_ASC_SORT = 'title_asc';
const TITLE_DESC_SORT = 'title_desc';
export const urlSortParams = {
@@ -187,7 +188,7 @@ export const urlSortParams = {
[WEIGHT_ASC]: WEIGHT,
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
- [TITLE_ASC]: TITLE,
+ [TITLE_ASC]: TITLE_ASC_SORT,
[TITLE_DESC]: TITLE_DESC_SORT,
};
@@ -211,6 +212,7 @@ export const TOKEN_TYPE_ASSIGNEE = 'assignee_username';
export const TOKEN_TYPE_MILESTONE = 'milestone';
export const TOKEN_TYPE_LABEL = 'labels';
export const TOKEN_TYPE_TYPE = 'type';
+export const TOKEN_TYPE_RELEASE = 'release';
export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_ITERATION = 'iteration';
@@ -271,6 +273,7 @@ export const filters = {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
+ [ALTERNATIVE_FILTER]: 'label_name',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
@@ -280,12 +283,28 @@ export const filters = {
[TOKEN_TYPE_TYPE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'types',
- [SPECIAL_FILTER]: 'types',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
- [SPECIAL_FILTER]: 'type[]',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[type][]',
+ },
+ },
+ },
+ [TOKEN_TYPE_RELEASE]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'releaseTag',
+ [SPECIAL_FILTER]: 'releaseTagWildcardId',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'release_tag',
+ [SPECIAL_FILTER]: 'release_tag',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[release_tag]',
},
},
},
@@ -299,6 +318,9 @@ export const filters = {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[my_reaction_emoji]',
+ },
},
},
[TOKEN_TYPE_CONFIDENTIAL]: {
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 47af20f5271..59034964afb 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -24,7 +24,7 @@ export function mountJiraIssuesListApp() {
}
Vue.use(VueApollo);
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+ const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
});
@@ -103,7 +103,7 @@ export function mountIssuesListApp() {
},
};
- const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true });
+ const defaultClient = createDefaultClient(resolvers);
const apolloProvider = new VueApollo({
defaultClient,
});
@@ -137,6 +137,7 @@ export function mountIssuesListApp() {
newIssuePath,
projectImportJiraPath,
quickActionsHelpPath,
+ releasesPath,
resetPath,
rssPath,
showNewIssueLink,
@@ -164,6 +165,7 @@ export function mountIssuesListApp() {
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
newIssuePath,
+ releasesPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
index 6df72cf6596..9866efbcecc 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -11,9 +11,13 @@ query getIssues(
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
$beforeCursor: String
@@ -30,9 +34,11 @@ query getIssues(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
before: $beforeCursor
@@ -57,9 +63,13 @@ query getIssues(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
before: $beforeCursor
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
index 7bcdbbb28fc..5e755ec5870 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
@@ -5,9 +5,13 @@ query getIssuesCount(
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $releaseTag: [String!]
+ $releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
) {
@@ -19,9 +23,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -34,9 +40,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -49,9 +57,11 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
@@ -65,9 +75,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
@@ -79,9 +93,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
@@ -93,9 +111,13 @@ query getIssuesCount(
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ releaseTag: $releaseTag
+ releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
index 8f9b888d19b..8c95e6114d3 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
@@ -1,4 +1,4 @@
-query($fullPath: ID!) {
+query getIssuesListDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
issues {
nodes {
diff --git a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
index 78a368089a8..4f7217be7f7 100644
--- a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
@@ -1,4 +1,10 @@
fragment Iteration on Iteration {
id
title
+ startDate
+ dueDate
+ iterationCadence {
+ id
+ title
+ }
}
diff --git a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
index df1f330139a..75463f643a2 100644
--- a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
@@ -3,6 +3,7 @@ query searchProjects($fullPath: ID!, $search: String) {
projects(search: $search, includeSubgroups: true) {
nodes {
id
+ issuesEnabled
name
nameWithNamespace
webUrl
diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js
index f96567ef53b..815f338f1a0 100644
--- a/app/assets/javascripts/issues_list/service_desk_helper.js
+++ b/app/assets/javascripts/issues_list/service_desk_helper.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
/**
* Generates empty state messages for Service Desk issues list.
@@ -20,12 +20,12 @@ export function generateMessages(emptyStateMeta) {
);
const serviceDeskSupportedMessage = s__(
- 'ServiceDesk|Issues created from Service Desk emails appear here. Each comment becomes part of the email conversation.',
+ 'ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation.',
);
const commonDescription = `
<span>${serviceDeskSupportedMessage}</span>
- <a href="${serviceDeskHelpPage}">${s__('Learn more.')}</a>`;
+ <a href="${serviceDeskHelpPage}">${__('Learn more.')}</a>`;
return {
serviceDeskEnabledAndCanEditProjectSettings: {
@@ -60,7 +60,7 @@ export function generateMessages(emptyStateMeta) {
'ServiceDesk|To enable Service Desk on this instance, an instance administrator must first set up incoming email.',
),
primaryLink: incomingEmailHelpPage,
- primaryText: s__('Learn more.'),
+ primaryText: __('Learn more.'),
},
serviceDeskIsNotEnabled: {
title: s__('ServiceDesk|Service Desk is not enabled'),
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
index 1d3d07475af..0e57e2bff83 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -21,9 +21,13 @@ import {
RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
+ TITLE_ASC,
+ TITLE_DESC,
TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
UPDATED_ASC,
UPDATED_DESC,
@@ -113,11 +117,19 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
descending: RELATIVE_POSITION_ASC,
},
},
+ {
+ id: 9,
+ title: __('Title'),
+ sortDirection: {
+ ascending: TITLE_ASC,
+ descending: TITLE_DESC,
+ },
+ },
];
if (hasIssueWeightsFeature) {
sortOptions.push({
- id: 9,
+ id: sortOptions.length + 1,
title: __('Weight'),
sortDirection: {
ascending: WEIGHT_ASC,
@@ -128,7 +140,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
if (hasBlockedIssuesFeature) {
sortOptions.push({
- id: 10,
+ id: sortOptions.length + 1,
title: __('Blocking'),
sortDirection: {
ascending: BLOCKING_ISSUES_DESC,
@@ -193,17 +205,23 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
+const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_RELEASE];
+
const isWildcardValue = (tokenType, value) =>
- (tokenType === TOKEN_TYPE_ITERATION || tokenType === TOKEN_TYPE_MILESTONE) &&
- SPECIAL_FILTER_VALUES.includes(value);
+ wildcardTokens.includes(tokenType) && SPECIAL_FILTER_VALUES.includes(value);
const requiresUpperCaseValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
-const formatData = (token) =>
- requiresUpperCaseValue(token.type, token.value.data)
- ? token.value.data.toUpperCase()
- : token.value.data;
+const formatData = (token) => {
+ if (requiresUpperCaseValue(token.type, token.value.data)) {
+ return token.value.data.toUpperCase();
+ }
+ if (token.type === TOKEN_TYPE_CONFIDENTIAL) {
+ return token.value.data === 'yes';
+ }
+ return token.value.data;
+};
export const convertToApiParams = (filterTokens) => {
const params = {};
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
index e768154e210..32fbc1113bc 100644
--- a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
+++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getProjects(
+query jiraGetProjects(
$search: String!
$after: String = ""
$first: Int!
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
index 95bd4f5c675..04510fcff4b 100644
--- a/app/assets/javascripts/jira_connect/branches/index.js
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -14,12 +14,7 @@ export default async function initJiraConnectBranches() {
const { initialBranchName, successStateSvgPath } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue
new file mode 100644
index 00000000000..0b286bc903f
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_button.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { ADD_NAMESPACE_MODAL_ID } from '../constants';
+import AddNamespaceModal from './add_namespace_modal/add_namespace_modal.vue';
+
+export default {
+ components: {
+ GlButton,
+ AddNamespaceModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ ADD_NAMESPACE_MODAL_ID,
+};
+</script>
+<template>
+ <div>
+ <gl-button v-gl-modal="$options.ADD_NAMESPACE_MODAL_ID" category="primary" variant="info">
+ {{ s__('Integrations|Add namespace') }}
+ </gl-button>
+ <add-namespace-modal />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue
new file mode 100644
index 00000000000..0e209a09b16
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { ADD_NAMESPACE_MODAL_ID } from '../../constants';
+import GroupsList from './groups_list.vue';
+
+export default {
+ components: { GlModal, GroupsList },
+ modal: {
+ id: ADD_NAMESPACE_MODAL_ID,
+ title: s__('Integrations|Link namespaces'),
+ cancelProps: {
+ text: __('Cancel'),
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-cancel="$options.modal.cancelProps"
+ >
+ <groups-list />
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
index 5a49d7c1a90..5a49d7c1a90 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
new file mode 100644
index 00000000000..005c3bcd0e3
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { addSubscription } from '~/jira_connect/subscriptions/api';
+import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
+import { s__ } from '~/locale';
+import GroupItemName from '../group_item_name.vue';
+
+export default {
+ components: {
+ GlButton,
+ GroupItemName,
+ },
+ inject: {
+ subscriptionsPath: {
+ default: '',
+ },
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ addSubscription(this.subscriptionsPath, this.group.full_path)
+ .then(() => {
+ persistAlert({
+ title: s__('Integrations|Namespace successfully linked'),
+ message: s__(
+ 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
+ ),
+ linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
+ variant: 'success',
+ });
+
+ reloadPage();
+ })
+ .catch((error) => {
+ this.$emit(
+ 'error',
+ error?.response?.data?.error ||
+ s__('Integrations|Failed to link namespace. Please try again.'),
+ );
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
+ <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
+ <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
+ <group-item-name :group="group" />
+ </div>
+
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ :loading="isLoading"
+ :disabled="disabled"
+ @click.prevent="onClick"
+ >
+ {{ __('Link') }}
+ </gl-button>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 413424be28d..c0504cbb645 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -1,65 +1,51 @@
<script>
-import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import { mapState, mapMutations } from 'vuex';
-import { retrieveAlert, getLocation } from '~/jira_connect/subscriptions/utils';
-import { __ } from '~/locale';
+import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { SET_ALERT } from '../store/mutation_types';
-import GroupsList from './groups_list.vue';
import SubscriptionsList from './subscriptions_list.vue';
+import AddNamespaceButton from './add_namespace_button.vue';
+import SignInButton from './sign_in_button.vue';
export default {
name: 'JiraConnectApp',
components: {
GlAlert,
- GlButton,
GlLink,
- GlModal,
GlSprintf,
- GroupsList,
+ GlEmptyState,
SubscriptionsList,
- },
- directives: {
- GlModalDirective,
+ AddNamespaceButton,
+ SignInButton,
},
inject: {
usersPath: {
default: '',
},
- },
- data() {
- return {
- location: '',
- };
+ subscriptions: {
+ default: [],
+ },
},
computed: {
...mapState(['alert']),
- usersPathWithReturnTo() {
- if (this.location) {
- return `${this.usersPath}?return_to=${this.location}`;
- }
-
- return this.usersPath;
- },
shouldShowAlert() {
return Boolean(this.alert?.message);
},
- },
- modal: {
- cancelProps: {
- text: __('Cancel'),
+ hasSubscriptions() {
+ return !isEmpty(this.subscriptions);
+ },
+ userSignedIn() {
+ return Boolean(!this.usersPath);
},
},
created() {
this.setInitialAlert();
- this.setLocation();
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
- async setLocation() {
- this.location = await getLocation();
- },
setInitialAlert() {
const { linkUrl, title, message, variant } = retrieveAlert() || {};
this.setAlert({ linkUrl, title, message, variant });
@@ -88,38 +74,44 @@ export default {
</template>
</gl-alert>
- <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
+ <template v-if="hasSubscriptions">
+ <div class="gl-display-flex gl-justify-content-end">
+ <sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
+ <add-namespace-button v-else />
+ </div>
- <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4">
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button
- v-if="usersPath"
- category="primary"
- variant="info"
- class="gl-align-self-center"
- :href="usersPathWithReturnTo"
- target="_blank"
- >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
+ <subscriptions-list />
+ </template>
+ <template v-else>
+ <div v-if="!userSignedIn" class="gl-text-center">
+ <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p>
+ <sign-in-button class="gl-mb-7" :users-path="usersPath">
+ {{ __('Sign in to GitLab') }}
+ </sign-in-button>
+ <p>
+ {{
+ s__(
+ 'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).',
+ )
+ }}
+ </p>
+ </div>
+ <gl-empty-state
+ v-else
+ :title="s__('Integrations|No linked namespaces')"
+ :description="
+ s__(
+ 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
+ )
+ "
>
- <template v-else>
- <gl-button
- v-gl-modal-directive="'add-namespace-modal'"
- category="primary"
- variant="info"
- class="gl-align-self-center"
- >{{ s__('Integrations|Add namespace') }}</gl-button
- >
- <gl-modal
- modal-id="add-namespace-modal"
- :title="s__('Integrations|Link namespaces')"
- :action-cancel="$options.modal.cancelProps"
- >
- <groups-list />
- </gl-modal>
- </template>
- </div>
-
- <subscriptions-list />
+ <template #actions>
+ <add-namespace-button />
+ </template>
+ </gl-empty-state>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
deleted file mode 100644
index ed7585e8a88..00000000000
--- a/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { addSubscription } from '~/jira_connect/subscriptions/api';
-import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import { s__ } from '~/locale';
-import GroupItemName from './group_item_name.vue';
-
-export default {
- components: {
- GlButton,
- GroupItemName,
- },
- inject: {
- subscriptionsPath: {
- default: '',
- },
- },
- props: {
- group: {
- type: Object,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- methods: {
- onClick() {
- this.isLoading = true;
-
- addSubscription(this.subscriptionsPath, this.group.full_path)
- .then(() => {
- persistAlert({
- title: s__('Integrations|Namespace successfully linked'),
- message: s__(
- 'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
- ),
- linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
- variant: 'success',
- });
-
- reloadPage();
- })
- .catch((error) => {
- this.$emit(
- 'error',
- error?.response?.data?.error ||
- s__('Integrations|Failed to link namespace. Please try again.'),
- );
- this.isLoading = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
- <div class="gl-display-flex gl-align-items-center gl-py-3">
- <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
- <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
- <group-item-name :group="group" />
- </div>
-
- <gl-button
- category="secondary"
- variant="confirm"
- :loading="isLoading"
- :disabled="disabled"
- @click.prevent="onClick"
- >
- {{ __('Link') }}
- </gl-button>
- </div>
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue
new file mode 100644
index 00000000000..dc0a77e99c2
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ usersPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ signInURL: '',
+ };
+ },
+ created() {
+ this.setSignInURL();
+ },
+ methods: {
+ async setSignInURL() {
+ this.signInURL = await getGitlabSignInURL(this.usersPath);
+ },
+ },
+};
+</script>
+<template>
+ <gl-button category="primary" variant="info" :href="signInURL" target="_blank">
+ <slot>
+ {{ s__('Integrations|Sign in to add namespaces') }}
+ </slot>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
index 7062fb370ed..33126040c16 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlButton, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapMutations } from 'vuex';
import { removeSubscription } from '~/jira_connect/subscriptions/api';
@@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue';
export default {
components: {
GlButton,
- GlEmptyState,
GlTable,
GroupItemName,
TimeagoTooltip,
@@ -44,17 +43,15 @@ export default {
},
],
i18n: {
- emptyTitle: s__('Integrations|No linked namespaces'),
- emptyDescription: s__(
- 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
- ),
unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
- isEmpty,
+ isUnlinkButtonDisabled(item) {
+ return !isEmpty(item);
+ },
isLoadingItem(item) {
return this.loadingItem === item;
},
@@ -81,29 +78,22 @@ export default {
</script>
<template>
- <div>
- <gl-empty-state
- v-if="isEmpty(subscriptions)"
- :title="$options.i18n.emptyTitle"
- :description="$options.i18n.emptyDescription"
- />
- <gl-table v-else :items="subscriptions" :fields="$options.fields">
- <template #cell(name)="{ item }">
- <group-item-name :group="item.group" />
- </template>
- <template #cell(created_at)="{ item }">
- <timeago-tooltip :time="item.created_at" />
- </template>
- <template #cell(actions)="{ item }">
- <gl-button
- :class="unlinkBtnClass(item)"
- category="secondary"
- :loading="isLoadingItem(item)"
- :disabled="!isEmpty(loadingItem)"
- @click.prevent="onClick(item)"
- >{{ __('Unlink') }}</gl-button
- >
- </template>
- </gl-table>
- </div>
+ <gl-table :items="subscriptions" :fields="$options.fields">
+ <template #cell(name)="{ item }">
+ <group-item-name :group="item.group" />
+ </template>
+ <template #cell(created_at)="{ item }">
+ <timeago-tooltip :time="item.created_at" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button
+ :class="unlinkBtnClass(item)"
+ category="secondary"
+ :loading="isLoadingItem(item)"
+ :disabled="isUnlinkButtonDisabled(loadingItem)"
+ @click.prevent="onClick(item)"
+ >{{ __('Unlink') }}</gl-button
+ >
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 8dff83eabb5..2a65b7bc1fa 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -1,3 +1,5 @@
export const DEFAULT_GROUPS_PER_PAGE = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
export const MINIMUM_SEARCH_TERM_LENGTH = 3;
+
+export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index f1262be0174..8a7a80d885d 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -7,16 +7,20 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
-import { getLocation, sizeToParent } from './utils';
+import { getGitlabSignInURL, sizeToParent } from './utils';
const store = createStore();
+/**
+ * Add `return_to` query param to all HAML-defined GitLab sign in links.
+ */
const updateSignInLinks = async () => {
- const location = await getLocation();
- Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
- const updatedLink = `${el.getAttribute('href')}?return_to=${location}`;
- el.setAttribute('href', updatedLink);
- });
+ await Promise.all(
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).map(async (el) => {
+ const updatedLink = await getGitlabSignInURL(el.getAttribute('href'));
+ el.setAttribute('href', updatedLink);
+ }),
+ );
};
export async function initJiraConnect() {
diff --git a/app/assets/javascripts/jira_connect/subscriptions/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js
index ed7a9484a81..b2d03a1fbba 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/utils.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js
@@ -1,4 +1,5 @@
import AccessorUtilities from '~/lib/utils/accessor';
+import { objectToQuery } from '~/lib/utils/url_utility';
import { ALERT_LOCALSTORAGE_KEY } from './constants';
const isFunction = (fn) => typeof fn === 'function';
@@ -71,3 +72,17 @@ export const sizeToParent = () => {
AP.sizeToParent();
}
};
+
+export const getGitlabSignInURL = async (signInURL) => {
+ const location = await getLocation();
+
+ if (location) {
+ const queryParams = {
+ return_to: location,
+ };
+
+ return `${signInURL}?${objectToQuery(queryParams)}`;
+ }
+
+ return signInURL;
+};
diff --git a/app/assets/javascripts/jira_import/index.js b/app/assets/javascripts/jira_import/index.js
index 003f3c7107e..695a237bf50 100644
--- a/app/assets/javascripts/jira_import/index.js
+++ b/app/assets/javascripts/jira_import/index.js
@@ -6,7 +6,7 @@ import App from './components/jira_import_app.vue';
Vue.use(VueApollo);
-const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
index 2aacc5cf668..6fec07cc6f8 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
@@ -1,6 +1,6 @@
#import "./jira_import.fragment.graphql"
-query($fullPath: ID!) {
+query getJiraImportDetails($fullPath: ID!) {
project(fullPath: $fullPath) {
jiraImportStatus
jiraImports {
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
index cca33af342c..7af30ffb869 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($input: JiraImportUsersInput!) {
+mutation getJiraUserMapping($input: JiraImportUsersInput!) {
jiraImportUsers(input: $input) {
jiraUsers {
jiraAccountId
diff --git a/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql b/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
index 807374bf06c..29f8428fbcf 100644
--- a/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
+++ b/app/assets/javascripts/jira_import/queries/initiate_jira_import.mutation.graphql
@@ -1,6 +1,6 @@
#import "./jira_import.fragment.graphql"
-mutation($input: JiraImportStartInput!) {
+mutation initiateJiraImport($input: JiraImportStartInput!) {
jiraImportStart(input: $input) {
jiraImport {
...JiraImport
diff --git a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
index 06f119e75ed..6ea8963e6a6 100644
--- a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
@@ -1,4 +1,4 @@
-query searchProjectMembers($fullPath: ID!, $search: String) {
+query jiraSearchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
projectMembers(search: $search) {
nodes {
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 269551ff9aa..7a52a1b0d6b 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlFormInput,
+ GlButton,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -8,6 +15,9 @@ import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
+ GlFormInputGroup,
+ GlInputGroupText,
+ GlFormInput,
GlButton,
GlLink,
GlSprintf,
@@ -32,6 +42,9 @@ export default {
value: 'value',
},
i18n: {
+ header: s__('CiVariables|Variables'),
+ keyLabel: s__('CiVariables|Key'),
+ valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
valuePlaceholder: s__('CiVariables|Input variable value'),
formHelpText: s__(
@@ -40,9 +53,13 @@ export default {
},
data() {
return {
- variables: [],
- key: '',
- secretValue: '',
+ variables: [
+ {
+ key: '',
+ secretValue: '',
+ id: uniqueId(),
+ },
+ ],
triggerBtnDisabled: false,
};
},
@@ -50,40 +67,32 @@ export default {
variableSettings() {
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
- },
- watch: {
- key(newVal) {
- this.handleValueChange(newVal, this.$options.inputTypes.key);
- },
- secretValue(newVal) {
- this.handleValueChange(newVal, this.$options.inputTypes.value);
+ preparedVariables() {
+ // we need to ensure no empty variables are passed to the API
+ // and secretValue should be snake_case when passed to the API
+ return this.variables
+ .filter((variable) => variable.key !== '')
+ .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
},
},
methods: {
...mapActions(['triggerManualJob']),
- handleValueChange(newValue, type) {
- if (newValue !== '') {
- this.createNewVariable(type);
- this.resetForm();
- }
+ canRemove(index) {
+ return index < this.variables.length - 1;
},
- createNewVariable(type) {
- const newVariable = {
- key: this.key,
- secret_value: this.secretValue,
- id: uniqueId(),
- };
+ addEmptyVariable() {
+ const lastVar = this.variables[this.variables.length - 1];
- this.variables.push(newVariable);
+ if (lastVar.key === '') {
+ return;
+ }
- return this.$nextTick().then(() => {
- this.$refs[`${this.$options.inputTypes[type]}-${newVariable.id}`][0].focus();
+ this.variables.push({
+ key: '',
+ secret_value: '',
+ id: uniqueId(),
});
},
- resetForm() {
- this.key = '';
- this.secretValue = '';
- },
deleteVariable(id) {
this.variables.splice(
this.variables.findIndex((el) => el.id === id),
@@ -93,112 +102,92 @@ export default {
trigger() {
this.triggerBtnDisabled = true;
- this.triggerManualJob(this.variables);
+ this.triggerManualJob(this.preparedVariables);
},
},
};
</script>
<template>
- <div class="col-12" data-testid="manual-vars-form">
- <label>{{ s__('CiVariables|Variables') }}</label>
-
- <div class="ci-table">
- <div class="gl-responsive-table-row table-row-header pb-0 pt-0 border-0" role="row">
- <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Key') }}</div>
- <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div>
- </div>
+ <div class="row gl-justify-content-center">
+ <div class="col-10" data-testid="manual-vars-form">
+ <label>{{ $options.i18n.header }}</label>
<div
- v-for="variable in variables"
+ v-for="(variable, index) in variables"
:key="variable.id"
- class="gl-responsive-table-row"
+ class="gl-display-flex gl-align-items-center gl-mb-4"
data-testid="ci-variable-row"
>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- :ref="`${$options.inputTypes.key}-${variable.id}`"
- v-model="variable.key"
- :placeholder="$options.i18n.keyPlaceholder"
- class="ci-variable-body-item form-control"
- data-testid="ci-variable-key"
- />
- </div>
- </div>
+ <gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
+ <template #prepend>
+ <gl-input-group-text>
+ {{ $options.i18n.keyLabel }}
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ :ref="`${$options.inputTypes.key}-${variable.id}`"
+ v-model="variable.key"
+ :placeholder="$options.i18n.keyPlaceholder"
+ data-testid="ci-variable-key"
+ @change="addEmptyVariable"
+ />
+ </gl-form-input-group>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- :ref="`${$options.inputTypes.value}-${variable.id}`"
- v-model="variable.secret_value"
- :placeholder="$options.i18n.valuePlaceholder"
- class="ci-variable-body-item form-control"
- data-testid="ci-variable-value"
- />
- </div>
- </div>
+ <gl-form-input-group class="gl-flex-grow-2">
+ <template #prepend>
+ <gl-input-group-text>
+ {{ $options.i18n.valueLabel }}
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ :ref="`${$options.inputTypes.value}-${variable.id}`"
+ v-model="variable.secretValue"
+ :placeholder="$options.i18n.valuePlaceholder"
+ data-testid="ci-variable-value"
+ />
+ </gl-form-input-group>
- <div class="table-section section-10">
- <div class="table-mobile-header" role="rowheader"></div>
- <div class="table-mobile-content justify-content-end">
- <gl-button
- category="tertiary"
- icon="clear"
- :aria-label="__('Delete variable')"
- data-testid="delete-variable-btn"
- @click="deleteVariable(variable.id)"
- />
- </div>
- </div>
+ <!-- delete variable button placeholder to not break flex layout -->
+ <div
+ v-if="!canRemove(index)"
+ class="gl-w-7 gl-mr-3"
+ data-testid="delete-variable-btn-placeholder"
+ ></div>
+
+ <gl-button
+ v-if="canRemove(index)"
+ class="gl-flex-grow-0 gl-flex-basis-0"
+ category="tertiary"
+ variant="danger"
+ icon="clear"
+ :aria-label="__('Delete variable')"
+ data-testid="delete-variable-btn"
+ @click="deleteVariable(variable.id)"
+ />
</div>
- <div class="gl-responsive-table-row">
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- ref="inputKey"
- v-model="key"
- class="js-input-key form-control"
- :placeholder="$options.i18n.keyPlaceholder"
- />
- </div>
- </div>
- <div class="table-section section-50">
- <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
- <div class="table-mobile-content gl-mr-3">
- <input
- ref="inputSecretValue"
- v-model="secretValue"
- class="ci-variable-body-item form-control"
- :placeholder="$options.i18n.valuePlaceholder"
- />
- </div>
- </div>
+ <div class="gl-text-center gl-mt-5">
+ <gl-sprintf :message="$options.i18n.formHelpText">
+ <template #link="{ content }">
+ <gl-link :href="variableSettings" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-button
+ class="gl-mt-5"
+ variant="info"
+ category="primary"
+ :aria-label="__('Trigger manual job')"
+ :disabled="triggerBtnDisabled"
+ data-testid="trigger-manual-job-btn"
+ @click="trigger"
+ >
+ {{ action.button_title }}
+ </gl-button>
</div>
- </div>
- <div class="gl-text-center gl-mt-3">
- <gl-sprintf :message="$options.i18n.formHelpText">
- <template #link="{ content }">
- <gl-link :href="variableSettings" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
- <div class="d-flex justify-content-center">
- <gl-button
- variant="info"
- category="primary"
- :aria-label="__('Trigger manual job')"
- :disabled="triggerBtnDisabled"
- data-testid="trigger-manual-job-btn"
- @click="trigger"
- >
- {{ action.button_title }}
- </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index fef5b37015c..b1ddede8fe8 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -84,9 +84,13 @@ export default {
>
</p>
- <gl-table :items="trigger.variables" :fields="$options.fields" small bordered>
+ <gl-table :items="trigger.variables" :fields="$options.fields" small bordered fixed>
+ <template #cell(key)="{ item }">
+ <span class="gl-overflow-break-word">{{ item.key }}</span>
+ </template>
+
<template #cell(value)="data">
- {{ getDisplayValue(data.value) }}
+ <span class="gl-overflow-break-word">{{ getDisplayValue(data.value) }}</span>
</template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
index ad92bd4de42..9b7901685b6 100644
--- a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
+++ b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js
@@ -9,10 +9,6 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
* @returns {ApolloLink|null}
*/
export const getSuppressNetworkErrorsDuringNavigationLink = () => {
- if (!gon.features?.suppressApolloErrorsDuringNavigation) {
- return null;
- }
-
return onError(({ networkError }) => {
if (networkError && isNavigatingAway()) {
// Return an observable that will never notify any subscribers with any
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 39bf804b54e..df2e85afe24 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -48,7 +48,6 @@ export const stripWhitespaceFromQuery = (url, path) => {
export default (resolvers = {}, config = {}) => {
const {
- assumeImmutableResults,
baseUrl,
batchMax = 10,
cacheConfig,
@@ -161,10 +160,10 @@ export default (resolvers = {}, config = {}) => {
link: appLink,
cache: new InMemoryCache({
...cacheConfig,
- freezeResults: assumeImmutableResults,
+ freezeResults: true,
}),
resolvers,
- assumeImmutableResults,
+ assumeImmutableResults: true,
defaultOptions: {
query: {
fetchPolicy,
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 813fd3dbb1e..a82dad7e2c9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -220,16 +220,16 @@ export const scrollToElement = (element, options = {}) => {
// In the previous implementation, jQuery naturally deferred this scrolling.
// Unfortunately, we're quite coupled to this implementation detail now.
defer(() => {
- const { duration = 200, offset = 0 } = options;
+ const { duration = 200, offset = 0, behavior = duration ? 'smooth' : 'auto' } = options;
const y = el.getBoundingClientRect().top + window.pageYOffset + offset - contentTop();
- window.scrollTo({ top: y, behavior: duration ? 'smooth' : 'auto' });
+ window.scrollTo({ top: y, behavior });
});
}
};
-export const scrollToElementWithContext = (element) => {
+export const scrollToElementWithContext = (element, options) => {
const offsetMultiplier = -0.1;
- return scrollToElement(element, { offset: window.innerHeight * offsetMultiplier });
+ return scrollToElement(element, { ...options, offset: window.innerHeight * offsetMultiplier });
};
/**
@@ -688,17 +688,20 @@ export const searchBy = (query = '', searchSpace = {}) => {
*/
export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER);
+const scopedLabelRegex = new RegExp(`(.*)${SCOPED_LABEL_DELIMITER}.*`);
+
/**
- * Returns the base value of the scoped label
- *
- * Expected Label to be an Object with `title` as a key:
- * { title: 'LabelTitle', ...otherProperties };
+ * Returns the key of a scoped label.
+ * For example:
+ * - returns `scoped` if the label is `scoped::value`.
+ * - returns `scoped::label` if the label is `scoped::label::value`.
*
- * @param {Object} label
- * @returns String
+ * @param {Object} label object containing `title` property
+ * @returns String scoped label key, or full label if it is not a scoped label
*/
-export const scopedLabelKey = ({ title = '' }) =>
- isScopedLabel({ title }) && title.split(SCOPED_LABEL_DELIMITER)[0];
+export const scopedLabelKey = ({ title = '' }) => {
+ return title.replace(scopedLabelRegex, '$1');
+};
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
new file mode 100644
index 00000000000..733d0f69f5d
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ cancelAction: { text: __('Cancel') },
+ components: {
+ GlModal,
+ },
+ props: {
+ primaryText: {
+ type: String,
+ required: false,
+ default: __('OK'),
+ },
+ primaryVariant: {
+ type: String,
+ required: false,
+ default: 'confirm',
+ },
+ },
+ computed: {
+ primaryAction() {
+ return { text: this.primaryText, attributes: { variant: this.primaryVariant } };
+ },
+ },
+ mounted() {
+ this.$refs.modal.show();
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ size="sm"
+ modal-id="confirmationModal"
+ body-class="gl-display-flex"
+ :action-primary="primaryAction"
+ :action-cancel="$options.cancelAction"
+ hide-header
+ @primary="$emit('confirmed')"
+ @hidden="$emit('closed')"
+ >
+ <div class="gl-align-self-center"><slot></slot></div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
new file mode 100644
index 00000000000..fdd0e045d07
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+
+export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) {
+ return new Promise((resolve) => {
+ let confirmed = false;
+
+ const component = new Vue({
+ components: {
+ ConfirmModal: () => import('./confirm_modal.vue'),
+ },
+ render(h) {
+ return h(
+ 'confirm-modal',
+ {
+ props: {
+ primaryVariant: primaryBtnVariant,
+ primaryText: primaryBtnText,
+ },
+ on: {
+ confirmed() {
+ confirmed = true;
+ },
+ closed() {
+ component.$destroy();
+ resolve(confirmed);
+ },
+ },
+ },
+ [message],
+ );
+ },
+ }).$mount();
+ });
+}
+
+export function confirmViaGlModal(message, element) {
+ const primaryBtnConfig = {};
+
+ const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
+
+ if (confirmBtnVariant) {
+ primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
+ }
+
+ const screenReaderText =
+ element.querySelector('.gl-sr-only')?.textContent ||
+ element.querySelector('.sr-only')?.textContent ||
+ element.getAttribute('aria-label');
+
+ if (screenReaderText) {
+ primaryBtnConfig.primaryBtnText = screenReaderText;
+ }
+
+ return confirmAction(message, primaryBtnConfig);
+}
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 0e5a23a5cbb..36c6545164e 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -24,3 +24,5 @@ export const DEFAULT_TH_CLASSES =
// We set the drawer's z-index to 252 to clear flash messages that might
// be displayed in the page and that have a z-index of 251.
export const DRAWER_Z_INDEX = 252;
+
+export const MIN_USERNAME_LENGTH = 2;
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index 3c446c21865..7bff2bf3e47 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -14,33 +14,33 @@ import { s__, n__, __, sprintf } from '../../../locale';
export const getMonthNames = (abbreviated) => {
if (abbreviated) {
return [
- s__('Jan'),
- s__('Feb'),
- s__('Mar'),
- s__('Apr'),
- s__('May'),
- s__('Jun'),
- s__('Jul'),
- s__('Aug'),
- s__('Sep'),
- s__('Oct'),
- s__('Nov'),
- s__('Dec'),
+ __('Jan'),
+ __('Feb'),
+ __('Mar'),
+ __('Apr'),
+ __('May'),
+ __('Jun'),
+ __('Jul'),
+ __('Aug'),
+ __('Sep'),
+ __('Oct'),
+ __('Nov'),
+ __('Dec'),
];
}
return [
- s__('January'),
- s__('February'),
- s__('March'),
- s__('April'),
- s__('May'),
- s__('June'),
- s__('July'),
- s__('August'),
- s__('September'),
- s__('October'),
- s__('November'),
- s__('December'),
+ __('January'),
+ __('February'),
+ __('March'),
+ __('April'),
+ __('May'),
+ __('June'),
+ __('July'),
+ __('August'),
+ __('September'),
+ __('October'),
+ __('November'),
+ __('December'),
];
};
diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js
index b8b63bf58d4..f99a4927338 100644
--- a/app/assets/javascripts/lib/utils/file_upload.js
+++ b/app/assets/javascripts/lib/utils/file_upload.js
@@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => {
});
};
-export const getFilename = ({ clipboardData }) => {
- let value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData('Text');
- } else if (clipboardData && clipboardData.getData) {
- value = clipboardData.getData('text/plain');
+export const getFilename = (file) => {
+ let fileName;
+ if (file) {
+ fileName = file.name;
}
- value = value.split('\r');
- return value[0];
+
+ return fileName;
+};
+
+export const validateImageName = (file) => {
+ const fileName = file.name ? file.name : 'image.png';
+ const legalImageRegex = /^[\w.\-+]+\.(png|jpg|jpeg|gif|bmp|tiff|ico|webp)$/;
+ return legalImageRegex.test(fileName) ? fileName : 'image.png';
};
diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js
index 8b40cc7bd11..6b1985a23ba 100644
--- a/app/assets/javascripts/lib/utils/rails_ujs.js
+++ b/app/assets/javascripts/lib/utils/rails_ujs.js
@@ -1,4 +1,42 @@
import Rails from '@rails/ujs';
+import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal';
+
+function monkeyPatchConfirmModal() {
+ /**
+ * This function is used to replace the `Rails.confirm` which uses `window.confirm`
+ *
+ * This function opens a confirmation modal which will resolve in a promise.
+ * Because the `Rails.confirm` API is synchronous, we go with a little hack here:
+ *
+ * 1. User clicks on something with `data-confirm`
+ * 2. We open the modal and return `false`, ending the "Rails" event chain
+ * 3. If the modal is closed and the user "confirmed" the action
+ * 1. replace the `Rails.confirm` with a function that always returns `true`
+ * 2. click the same element programmatically
+ *
+ * @param message {String} Message to be shown in the modal
+ * @param element {HTMLElement} Element that was clicked on
+ * @returns {boolean}
+ */
+ function confirmViaModal(message, element) {
+ confirmViaGlModal(message, element)
+ .then((confirmed) => {
+ if (confirmed) {
+ Rails.confirm = () => true;
+ element.click();
+ Rails.confirm = confirmViaModal;
+ }
+ })
+ .catch(() => {});
+ return false;
+ }
+
+ Rails.confirm = confirmViaModal;
+}
+
+if (gon?.features?.bootstrapConfirmationModals) {
+ monkeyPatchConfirmModal();
+}
export const initRails = () => {
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 0804d792631..40dd29bea76 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -233,7 +233,7 @@ export function insertMarkdownText({
}
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, () =>
- selected.replace(/\\n/g, '\n').replace('%br', '\\n'),
+ selected.replace(/\\n/g, '\n').replace(/%br/g, '\\n'),
);
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index c70d23d06ec..e53a39cde06 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -5,6 +5,12 @@ const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
const SHA_REGEX = /[\da-f]{40}/gi;
+// About GitLab default host (overwrite in jh)
+export const PROMO_HOST = 'about.gitlab.com';
+
+// About Gitlab default url (overwrite in jh)
+export const PROMO_URL = `https://${PROMO_HOST}`;
+
// Reset the cursor in a Regex so that multiple uses before a recompile don't fail
function resetRegExp(regex) {
regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 2a60825a427..c9e7b034950 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -130,7 +130,7 @@ export default {
}}
<a :href="clusterApplicationsDocumentationPath">
<strong>
- {{ s__('View Documentation') }}
+ {{ __('View Documentation') }}
</strong>
</a>
</gl-alert>
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index 0ec39f58930..c5083bc4826 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -11,7 +11,10 @@ export default {
components: { MembersTable, FilterSortContainer, GlAlert },
provide() {
return {
- namespace: this.namespace,
+ // We can't use this.namespace due to bug in vue-apollo when
+ // provide is called in beforeCreate
+ // See https://github.com/vuejs/vue-apollo/pull/1153 for details
+ namespace: this.$options.propsData.namespace,
};
},
props: {
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
deleted file mode 100644
index e5d7e2ea2eb..00000000000
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-<script>
-import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
-import { values, get } from 'lodash';
-import createFlash from '~/flash';
-import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { s__ } from '~/locale';
-import { OPERATORS } from '../constants';
-import AlertsService from '../services/alerts_service';
-import { alertsValidator, queriesValidator } from '../validators';
-import AlertWidgetForm from './alert_widget_form.vue';
-
-export default {
- components: {
- AlertWidgetForm,
- GlBadge,
- GlLoadingIcon,
- GlIcon,
- GlTooltip,
- GlSprintf,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- alertsEndpoint: {
- type: String,
- required: true,
- },
- showLoadingState: {
- type: Boolean,
- required: false,
- default: true,
- },
- // { [alertPath]: { alert_attributes } }. Populated from subsequent API calls.
- // Includes only the metrics/alerts to be managed by this widget.
- alertsToManage: {
- type: Object,
- required: false,
- default: () => ({}),
- validator: alertsValidator,
- },
- // [{ metric+query_attributes }]. Represents queries (and alerts) we know about
- // on intial fetch. Essentially used for reference.
- relevantQueries: {
- type: Array,
- required: true,
- validator: queriesValidator,
- },
- modalId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- service: null,
- errorMessage: null,
- isLoading: false,
- apiAction: 'create',
- };
- },
- i18n: {
- alertsCountMsg: s__('PrometheusAlerts|%{count} alerts applied'),
- singleFiringMsg: s__('PrometheusAlerts|Firing: %{alert}'),
- multipleFiringMsg: s__('PrometheusAlerts|%{firingCount} firing'),
- firingAlertsTooltip: s__('PrometheusAlerts|Firing: %{alerts}'),
- },
- computed: {
- singleAlertSummary() {
- return {
- message: this.isFiring ? this.$options.i18n.singleFiringMsg : this.thresholds[0],
- alert: this.thresholds[0],
- };
- },
- multipleAlertsSummary() {
- return {
- message: this.isFiring
- ? `${this.$options.i18n.alertsCountMsg}, ${this.$options.i18n.multipleFiringMsg}`
- : this.$options.i18n.alertsCountMsg,
- count: this.thresholds.length,
- firingCount: this.firingAlerts.length,
- };
- },
- shouldShowLoadingIcon() {
- return this.showLoadingState && this.isLoading;
- },
- thresholds() {
- const alertsToManage = Object.keys(this.alertsToManage);
- return alertsToManage.map(this.formatAlertSummary);
- },
- hasAlerts() {
- return Boolean(Object.keys(this.alertsToManage).length);
- },
- hasMultipleAlerts() {
- return this.thresholds.length > 1;
- },
- isFiring() {
- return Boolean(this.firingAlerts.length);
- },
- firingAlerts() {
- return values(this.alertsToManage).filter((alert) =>
- this.passedAlertThreshold(this.getQueryData(alert), alert),
- );
- },
- formattedFiringAlerts() {
- return this.firingAlerts.map((alert) => this.formatAlertSummary(alert.alert_path));
- },
- configuredAlert() {
- return this.hasAlerts ? values(this.alertsToManage)[0].metricId : '';
- },
- },
- created() {
- this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint });
- this.fetchAlertData();
- },
- methods: {
- fetchAlertData() {
- this.isLoading = true;
-
- const queriesWithAlerts = this.relevantQueries.filter((query) => query.alert_path);
-
- return Promise.all(
- queriesWithAlerts.map((query) =>
- this.service
- .readAlert(query.alert_path)
- .then((alertAttributes) => this.setAlert(alertAttributes, query.metricId)),
- ),
- )
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- createFlash({
- message: s__('PrometheusAlerts|Error fetching alert'),
- });
- this.isLoading = false;
- });
- },
- setAlert(alertAttributes, metricId) {
- this.$emit('setAlerts', alertAttributes.alert_path, { ...alertAttributes, metricId });
- },
- removeAlert(alertPath) {
- this.$emit('setAlerts', alertPath, null);
- },
- formatAlertSummary(alertPath) {
- const alert = this.alertsToManage[alertPath];
- const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
-
- return `${alertQuery.label} ${alert.operator} ${alert.threshold}`;
- },
- passedAlertThreshold(data, alert) {
- const { threshold, operator } = alert;
-
- switch (operator) {
- case OPERATORS.greaterThan:
- return data.some((value) => value > threshold);
- case OPERATORS.lessThan:
- return data.some((value) => value < threshold);
- case OPERATORS.equalTo:
- return data.some((value) => value === threshold);
- default:
- return false;
- }
- },
- getQueryData(alert) {
- const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
-
- return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null));
- },
- showModal() {
- this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- },
- hideModal() {
- this.errorMessage = null;
- this.$root.$emit(BV_HIDE_MODAL, this.modalId);
- },
- handleSetApiAction(apiAction) {
- this.apiAction = apiAction;
- },
- handleCreate({ operator, threshold, prometheus_metric_id, runbookUrl }) {
- const newAlert = { operator, threshold, prometheus_metric_id, runbookUrl };
- this.isLoading = true;
- this.service
- .createAlert(newAlert)
- .then((alertAttributes) => {
- this.setAlert(alertAttributes, prometheus_metric_id);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error creating alert');
- this.isLoading = false;
- });
- },
- handleUpdate({ alert, operator, threshold, runbookUrl }) {
- const updatedAlert = { operator, threshold, runbookUrl };
- this.isLoading = true;
- this.service
- .updateAlert(alert, updatedAlert)
- .then((alertAttributes) => {
- this.setAlert(alertAttributes, this.alertsToManage[alert].metricId);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error saving alert');
- this.isLoading = false;
- });
- },
- handleDelete({ alert }) {
- this.isLoading = true;
- this.service
- .deleteAlert(alert)
- .then(() => {
- this.removeAlert(alert);
- this.isLoading = false;
- this.hideModal();
- })
- .catch(() => {
- this.errorMessage = s__('PrometheusAlerts|Error deleting alert');
- this.isLoading = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="prometheus-alert-widget dropdown flex-grow-2 overflow-hidden">
- <gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" size="sm" />
- <span v-else-if="errorMessage" ref="alertErrorMessage" class="alert-error-message">{{
- errorMessage
- }}</span>
- <span
- v-else-if="hasAlerts"
- ref="alertCurrentSetting"
- class="alert-current-setting cursor-pointer d-flex"
- @click="showModal"
- >
- <gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate">
- <gl-icon name="warning" :size="16" class="flex-shrink-0" />
- <span class="text-truncate gl-pl-2">
- <gl-sprintf
- :message="
- hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
- "
- >
- <template #alert>
- {{ singleAlertSummary.alert }}
- </template>
- <template #count>
- {{ multipleAlertsSummary.count }}
- </template>
- <template #firingCount>
- {{ multipleAlertsSummary.firingCount }}
- </template>
- </gl-sprintf>
- </span>
- </gl-badge>
- <gl-tooltip v-if="hasMultipleAlerts && isFiring" :target="() => $refs.alertCurrentSetting">
- <gl-sprintf :message="$options.i18n.firingAlertsTooltip">
- <template #alerts>
- <div v-for="alert in formattedFiringAlerts" :key="alert.alert_path">
- {{ alert }}
- </div>
- </template>
- </gl-sprintf>
- </gl-tooltip>
- </span>
- <alert-widget-form
- ref="widgetForm"
- :disabled="isLoading"
- :alerts-to-manage="alertsToManage"
- :relevant-queries="relevantQueries"
- :error-message="errorMessage"
- :configured-alert="configuredAlert"
- :modal-id="modalId"
- @create="handleCreate"
- @update="handleUpdate"
- @delete="handleDelete"
- @cancel="hideModal"
- @setAction="handleSetApiAction"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
deleted file mode 100644
index 68fd3e256ec..00000000000
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ /dev/null
@@ -1,324 +0,0 @@
-<script>
-import {
- GlLink,
- GlButton,
- GlButtonGroup,
- GlFormGroup,
- GlFormInput,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlTooltipDirective,
- GlIcon,
-} from '@gitlab/ui';
-import { isEmpty, findKey } from 'lodash';
-import Vue from 'vue';
-import { __, s__ } from '~/locale';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import Translate from '~/vue_shared/translate';
-import { OPERATORS } from '../constants';
-import { alertsValidator, queriesValidator } from '../validators';
-
-Vue.use(Translate);
-
-const SUBMIT_ACTION_TEXT = {
- create: __('Add'),
- update: __('Save'),
- delete: __('Delete'),
-};
-
-const SUBMIT_BUTTON_CLASS = {
- create: 'btn-success',
- update: 'btn-success',
- delete: 'btn-danger',
-};
-
-export default {
- components: {
- GlButton,
- GlButtonGroup,
- GlFormGroup,
- GlFormInput,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlLink,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
- },
- mixins: [glFeatureFlagsMixin()],
- props: {
- disabled: {
- type: Boolean,
- required: true,
- },
- errorMessage: {
- type: String,
- required: false,
- default: '',
- },
- configuredAlert: {
- type: String,
- required: false,
- default: '',
- },
- alertsToManage: {
- type: Object,
- required: false,
- default: () => ({}),
- validator: alertsValidator,
- },
- relevantQueries: {
- type: Array,
- required: true,
- validator: queriesValidator,
- },
- modalId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- operators: OPERATORS,
- operator: null,
- threshold: null,
- prometheusMetricId: null,
- runbookUrl: null,
- selectedAlert: {},
- alertQuery: '',
- };
- },
- computed: {
- isValidQuery() {
- // TODO: Add query validation check (most likely via http request)
- return this.alertQuery.length ? true : null;
- },
- currentQuery() {
- return this.relevantQueries.find((query) => query.metricId === this.prometheusMetricId) || {};
- },
- formDisabled() {
- // We need a prometheusMetricId to determine whether we're
- // creating/updating/deleting
- return this.disabled || !(this.prometheusMetricId || this.isValidQuery);
- },
- supportsComputedAlerts() {
- return this.glFeatures.prometheusComputedAlerts;
- },
- queryDropdownLabel() {
- return this.currentQuery.label || s__('PrometheusAlerts|Select query');
- },
- haveValuesChanged() {
- return (
- this.operator &&
- this.threshold === Number(this.threshold) &&
- (this.operator !== this.selectedAlert.operator ||
- this.threshold !== this.selectedAlert.threshold ||
- this.runbookUrl !== this.selectedAlert.runbookUrl)
- );
- },
- submitAction() {
- if (isEmpty(this.selectedAlert)) return 'create';
- if (this.haveValuesChanged) return 'update';
- return 'delete';
- },
- submitActionText() {
- return SUBMIT_ACTION_TEXT[this.submitAction];
- },
- submitButtonClass() {
- return SUBMIT_BUTTON_CLASS[this.submitAction];
- },
- isSubmitDisabled() {
- return this.disabled || (this.submitAction === 'create' && !this.haveValuesChanged);
- },
- dropdownTitle() {
- return this.submitAction === 'create'
- ? s__('PrometheusAlerts|Add alert')
- : s__('PrometheusAlerts|Edit alert');
- },
- },
- watch: {
- alertsToManage() {
- this.resetAlertData();
- },
- submitAction() {
- this.$emit('setAction', this.submitAction);
- },
- },
- methods: {
- selectQuery(queryId) {
- const existingAlertPath = findKey(this.alertsToManage, (alert) => alert.metricId === queryId);
- const existingAlert = this.alertsToManage[existingAlertPath];
-
- if (existingAlert) {
- const { operator, threshold, runbookUrl } = existingAlert;
-
- this.selectedAlert = existingAlert;
- this.operator = operator;
- this.threshold = threshold;
- this.runbookUrl = runbookUrl;
- } else {
- this.selectedAlert = {};
- this.operator = this.operators.greaterThan;
- this.threshold = null;
- this.runbookUrl = null;
- }
-
- this.prometheusMetricId = queryId;
- },
- handleHidden() {
- this.resetAlertData();
- this.$emit('cancel');
- },
- handleSubmit() {
- this.$emit(this.submitAction, {
- alert: this.selectedAlert.alert_path,
- operator: this.operator,
- threshold: this.threshold,
- prometheus_metric_id: this.prometheusMetricId,
- runbookUrl: this.runbookUrl,
- });
- },
- handleShown() {
- if (this.configuredAlert) {
- this.selectQuery(this.configuredAlert);
- } else if (this.relevantQueries.length === 1) {
- this.selectQuery(this.relevantQueries[0].metricId);
- }
- },
- resetAlertData() {
- this.operator = null;
- this.threshold = null;
- this.prometheusMetricId = null;
- this.selectedAlert = {};
- this.runbookUrl = null;
- },
- getAlertFormActionTrackingOption() {
- const label = `${this.submitAction}_alert`;
- return {
- category: document.body.dataset.page,
- action: 'click_button',
- label,
- };
- },
- },
- alertQueryText: {
- label: __('Query'),
- validFeedback: __('Query is valid'),
- invalidFeedback: __('Invalid query'),
- descriptionTooltip: __(
- 'Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula.',
- ),
- },
-};
-</script>
-
-<template>
- <gl-modal
- ref="alertModal"
- :title="dropdownTitle"
- :modal-id="modalId"
- :ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
- :ok-disabled="formDisabled"
- @ok.prevent="handleSubmit"
- @hidden="handleHidden"
- @shown="handleShown"
- >
- <div v-if="errorMessage" class="alert-modal-message danger_message">{{ errorMessage }}</div>
- <div class="alert-form">
- <gl-form-group
- v-if="supportsComputedAlerts"
- :label="$options.alertQueryText.label"
- label-for="alert-query-input"
- :valid-feedback="$options.alertQueryText.validFeedback"
- :invalid-feedback="$options.alertQueryText.invalidFeedback"
- :state="isValidQuery"
- >
- <gl-form-input id="alert-query-input" v-model.trim="alertQuery" :state="isValidQuery" />
- <template #description>
- <div class="d-flex align-items-center">
- {{ __('Single or combined queries') }}
- <gl-icon
- v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
- name="question"
- class="gl-ml-2"
- />
- </div>
- </template>
- </gl-form-group>
- <gl-form-group v-else label-for="alert-query-dropdown" :label="$options.alertQueryText.label">
- <gl-dropdown
- id="alert-query-dropdown"
- :text="queryDropdownLabel"
- toggle-class="dropdown-menu-toggle gl-border-1! qa-alert-query-dropdown"
- >
- <gl-dropdown-item
- v-for="query in relevantQueries"
- :key="query.metricId"
- data-qa-selector="alert_query_option"
- @click="selectQuery(query.metricId)"
- >
- {{ query.label }}
- </gl-dropdown-item>
- </gl-dropdown>
- </gl-form-group>
- <gl-button-group class="mb-3" :label="s__('PrometheusAlerts|Operator')">
- <gl-button
- :class="{ active: operator === operators.greaterThan }"
- :disabled="formDisabled"
- @click="operator = operators.greaterThan"
- >
- {{ operators.greaterThan }}
- </gl-button>
- <gl-button
- :class="{ active: operator === operators.equalTo }"
- :disabled="formDisabled"
- @click="operator = operators.equalTo"
- >
- {{ operators.equalTo }}
- </gl-button>
- <gl-button
- :class="{ active: operator === operators.lessThan }"
- :disabled="formDisabled"
- @click="operator = operators.lessThan"
- >
- {{ operators.lessThan }}
- </gl-button>
- </gl-button-group>
- <gl-form-group :label="s__('PrometheusAlerts|Threshold')" label-for="alerts-threshold">
- <gl-form-input
- id="alerts-threshold"
- v-model.number="threshold"
- :disabled="formDisabled"
- type="number"
- data-qa-selector="alert_threshold_field"
- />
- </gl-form-group>
- <gl-form-group
- :label="s__('PrometheusAlerts|Runbook URL (optional)')"
- label-for="alert-runbook"
- >
- <gl-form-input
- id="alert-runbook"
- v-model="runbookUrl"
- :disabled="formDisabled"
- data-testid="alertRunbookField"
- type="text"
- :placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
- />
- </gl-form-group>
- </div>
- <template #modal-ok>
- <gl-link
- v-track-event="getAlertFormActionTrackingOption()"
- class="text-reset text-decoration-none"
- >
- {{ submitActionText }}
- </gl-link>
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index 4b54cffe231..ae079da0b0b 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,8 +1,12 @@
<script>
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { chartHeight } from '../../constants';
export default {
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
height: chartHeight,
@@ -18,14 +22,15 @@ export default {
created() {
this.chartEmptyStateIllustration = chartEmptyStateIllustration;
},
+ safeHtmlConfig: { ADD_TAGS: ['use'] },
};
</script>
<template>
<div class="d-flex flex-column justify-content-center">
<div
+ v-safe-html:[$options.safeHtmlConfig]="chartEmptyStateIllustration"
class="gl-mt-3 svg-w-100 d-flex align-items-center"
:style="svgContainerStyle"
- v-html="chartEmptyStateIllustration /* eslint-disable-line vue/no-v-html */"
></div>
<h5 class="text-center gl-mt-3">{{ __('No data to display') }}</h5>
</div>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 12f5e7efc96..5529a94874b 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -73,11 +73,6 @@ export default {
required: false,
default: chartHeight,
},
- thresholds: {
- type: Array,
- required: false,
- default: () => [],
- },
legendLayout: {
type: String,
required: false,
@@ -391,7 +386,6 @@ export default {
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:format-annotations-tooltip-text="formatAnnotationsTooltipText"
- :thresholds="thresholds"
:width="width"
:height="height"
:legend-layout="legendLayout"
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index be9f104b81e..c9767330b73 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -8,10 +8,8 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import { defaultTimeRange } from '~/vue_shared/constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { metricStates, keyboardShortcutKeys } from '../constants';
import {
timeRangeFromUrl,
@@ -30,7 +28,6 @@ import VariablesSection from './variables_section.vue';
export default {
components: {
- AlertsDeprecationWarning,
VueDraggable,
DashboardHeader,
DashboardPanel,
@@ -47,7 +44,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
hasMetrics: {
type: Boolean,
@@ -399,8 +395,6 @@ export default {
<template>
<div class="prometheus-graphs" data-qa-selector="prometheus_graphs">
- <alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
-
<dashboard-header
v-if="showHeader"
ref="prometheusGraphsHeader"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 446c6b52602..78e3b15913a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -13,20 +13,16 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
-import { mapValues, pickBy } from 'lodash';
import { mapState } from 'vuex';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { panelTypes } from '../constants';
import { graphDataToCsv } from '../csv_export';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-import AlertWidget from './alert_widget.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorBarChart from './charts/bar.vue';
import MonitorColumnChart from './charts/column.vue';
@@ -45,7 +41,6 @@ const events = {
export default {
components: {
MonitorEmptyChart,
- AlertWidget,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -62,7 +57,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
clipboardText: {
type: String,
@@ -84,16 +78,6 @@ export default {
required: false,
default: 'monitoringDashboard',
},
- alertsEndpoint: {
- type: String,
- required: false,
- default: null,
- },
- prometheusAlertsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
settingsPath: {
type: String,
required: false,
@@ -104,7 +88,6 @@ export default {
return {
showTitleTooltip: false,
zoomedTimeRange: null,
- allAlerts: {},
expandBtnAvailable: Boolean(this.$listeners[events.expand]),
};
},
@@ -211,7 +194,7 @@ export default {
/**
* In monitoring, Time Series charts typically support
* a larger feature set like "annotations", "deployment
- * data", alert "thresholds" and "datazoom".
+ * data" and "datazoom".
*
* This is intentional as Time Series are more frequently
* used.
@@ -252,34 +235,11 @@ export default {
const { metrics = [] } = this.graphData;
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
- alertWidgetAvailable() {
- const supportsAlerts =
- this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
- return (
- supportsAlerts &&
- this.prometheusAlertsAvailable &&
- this.alertsEndpoint &&
- this.graphData &&
- this.hasMetricsInDb &&
- !this.glFeatures.managedAlertsDeprecation
- );
- },
- alertModalId() {
- return `alert-modal-${this.graphData.id}`;
- },
},
mounted() {
this.refreshTitleTooltip();
},
methods: {
- getGraphAlerts(queries) {
- if (!this.allAlerts) return {};
- const metricIdsForChart = queries.map((q) => q.metricId);
- return pickBy(this.allAlerts, (alert) => metricIdsForChart.includes(alert.metricId));
- },
- getGraphAlertValues(queries) {
- return Object.values(this.getGraphAlerts(queries));
- },
isPanelType(type) {
return this.graphData?.type === type;
},
@@ -310,24 +270,9 @@ export default {
this.onExpand();
}
},
- setAlerts(alertPath, alertAttributes) {
- if (alertAttributes) {
- this.$set(this.allAlerts, alertPath, alertAttributes);
- } else {
- this.$delete(this.allAlerts, alertPath);
- }
- },
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
- showAlertModal() {
- this.$root.$emit(BV_SHOW_MODAL, this.alertModalId);
- },
- showAlertModalFromKeyboardShortcut() {
- if (this.isContextualMenuShown) {
- this.showAlertModal();
- }
- },
visitLogsPage() {
if (this.logsPathWithTimeRange) {
visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
@@ -348,19 +293,6 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
- getAlertRunbooks(queries) {
- const hasRunbook = (alert) => Boolean(alert.runbookUrl);
- const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
- const alertToRunbookTransform = (alert) => {
- const alertQuery = queries.find((query) => query.metricId === alert.metricId);
- return {
- key: alert.metricId,
- href: alert.runbookUrl,
- label: alertQuery.label,
- };
- };
- return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
- },
},
panelTypes,
};
@@ -378,15 +310,6 @@ export default {
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }}
</gl-tooltip>
- <alert-widget
- v-if="isContextualMenuShown && alertWidgetAvailable"
- class="mx-1"
- :modal-id="alertModalId"
- :alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.metrics"
- :alerts-to-manage="getGraphAlerts(graphData.metrics)"
- @setAlerts="setAlerts"
- />
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon size="sm" />
@@ -450,32 +373,6 @@ export default {
>
{{ __('Copy link to chart') }}
</gl-dropdown-item>
- <gl-dropdown-item
- v-if="alertWidgetAvailable"
- v-gl-modal="alertModalId"
- data-qa-selector="alert_widget_menu_item"
- >
- {{ __('Alerts') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-for="runbook in getAlertRunbooks(graphData.metrics)"
- :key="runbook.key"
- :href="safeUrl(runbook.href)"
- data-testid="runbookLink"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
- <span>
- <gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
- <template #label>
- {{ runbook.label }}
- </template>
- </gl-sprintf>
- </span>
- <gl-icon name="external-link" />
- </span>
- </gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
@@ -515,7 +412,6 @@ export default {
:deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath"
- :thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
:time-range="fixedCurrentTimeRange"
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index 1765a2f3d5d..a63008aa382 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -63,7 +63,7 @@ export default {
return !(this.form.fileName && !this.form.fileName.endsWith('.yml'));
},
fileNameFeedback() {
- return !this.fileNameState ? s__('The file name should have a .yml extension') : '';
+ return !this.fileNameState ? __('The file name should have a .yml extension') : '';
},
},
mounted() {
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index cf79e71b9e0..ee67e5dd827 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -12,10 +12,7 @@ export default (props = {}) => {
if (el && el.dataset) {
const { metricsDashboardBasePath, ...dataset } = el.dataset;
- const {
- initState,
- dataProps: { hasManagedPrometheus, ...dataProps },
- } = stateAndPropsFromDataset(dataset);
+ const { initState, dataProps } = stateAndPropsFromDataset(dataset);
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
@@ -24,7 +21,6 @@ export default (props = {}) => {
el,
store,
router,
- provide: { hasManagedPrometheus },
data() {
return {
dashboardProps: { ...dataProps, ...props },
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 74b777d7b44..336b613b620 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -41,7 +41,6 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
- dataProps.hasManagedPrometheus = parseBoolean(dataProps.hasManagedPrometheus);
return {
initState: {
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index ea3e4e5604c..a1377415efe 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -23,8 +23,8 @@ export default function initMrNotes() {
initNotesApp();
document.addEventListener('merged:UpdateActions', () => {
- initRevertCommitModal();
- initCherryPickCommitModal();
+ initRevertCommitModal('i_code_review_post_merge_submit_revert_modal');
+ initCherryPickCommitModal('i_code_review_post_merge_submit_cherry_pick_modal');
});
requestIdleCallback(() => {
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 6e46c5d3c1f..714cf67e0bd 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -48,12 +48,7 @@ export default (elements) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
const listenerAddedAttr = 'data-mr-listener-added';
diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue
index c8f2f0bfb10..a80fda96363 100644
--- a/app/assets/javascripts/nav/components/responsive_home.vue
+++ b/app/assets/javascripts/nav/components/responsive_home.vue
@@ -55,6 +55,7 @@ export default {
v-gl-tooltip="{ title: newDropdownViewModel.title }"
:view-model="newDropdownViewModel"
class="gl-ml-3"
+ data-qa-selector="mobile_new_dropdown"
/>
</header>
<top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
index 154bed81854..bfcdcfc7292 100644
--- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
+++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
@@ -46,6 +46,7 @@ export default {
link-class="top-nav-menu-item"
:href="menuItem.href"
data-testid="item"
+ :data-qa-selector="`${menuItem.title.toLowerCase().replace(' ', '_')}_mobile_button`"
>
{{ menuItem.title }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 6ee5d85a09f..54fe9d19002 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -272,6 +272,8 @@ export default class BranchGraph {
return r
.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split('\n')[0])
.attr({
+ fill: 'currentColor',
+ class: 'gl-text-body',
'text-anchor': 'start',
font: '14px Monaco, monospace',
});
diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue
index db9e61dce82..d0ed963b55d 100644
--- a/app/assets/javascripts/notebook/cells/output/latex.vue
+++ b/app/assets/javascripts/notebook/cells/output/latex.vue
@@ -1,4 +1,5 @@
<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
@@ -7,6 +8,9 @@ export default {
components: {
Prompt,
},
+ directives: {
+ SafeHtml,
+ },
props: {
count: {
type: Number,
@@ -33,13 +37,16 @@ export default {
return svg.outerHTML;
},
},
+ safeHtmlConfig: {
+ // to support SVGs and custom tags for mathjax
+ ADD_TAGS: ['use', 'mjx-container', 'mjx-tool', 'mjx-status', 'mjx-tip'],
+ },
};
</script>
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="index === 0" />
- <!-- eslint-disable -->
- <div ref="maths" v-html="code"></div>
+ <div ref="maths" v-safe-html:[$options.safeHtmlConfig]="code"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 831e6dd8f92..33819c78c0f 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -78,8 +78,8 @@ export default {
v-if="resolveAllDiscussionsIssuePath && !allResolved"
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
- :title="s__('Create issue to resolve all threads')"
- :aria-label="s__('Create issue to resolve all threads')"
+ :title="__('Create issue to resolve all threads')"
+ :aria-label="__('Create issue to resolve all threads')"
class="new-issue-for-discussion discussion-create-issue-btn"
icon="issue-new"
/>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..d1df4eb848b 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import { GlIntersectionObserver } from '@gitlab/ui';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
@@ -16,7 +17,9 @@ export default {
ToggleRepliesWidget,
NoteEditedText,
DiscussionNotesRepliesWrapper,
+ GlIntersectionObserver,
},
+ inject: ['discussionObserverHandler'],
props: {
discussion: {
type: Object,
@@ -54,7 +57,11 @@ export default {
},
},
computed: {
- ...mapGetters(['userCanReply']),
+ ...mapGetters([
+ 'userCanReply',
+ 'previousUnresolvedDiscussionId',
+ 'firstUnresolvedDiscussionId',
+ ]),
hasReplies() {
return Boolean(this.replies.length);
},
@@ -77,9 +84,20 @@ export default {
url: this.discussion.discussion_path,
};
},
+ isFirstUnresolved() {
+ return this.firstUnresolvedDiscussionId === this.discussion.id;
+ },
+ },
+ observerOptions: {
+ threshold: 0,
+ rootMargin: '0px 0px -50% 0px',
},
methods: {
- ...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
+ ...mapActions([
+ 'toggleDiscussion',
+ 'setSelectedCommentPositionHover',
+ 'setCurrentDiscussionId',
+ ]),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@@ -110,6 +128,18 @@ export default {
this.setSelectedCommentPositionHover();
}
},
+ observerTriggered(entry) {
+ this.discussionObserverHandler({
+ entry,
+ isFirstUnresolved: this.isFirstUnresolved,
+ currentDiscussion: { ...this.discussion },
+ isDiffsPage: !this.isOverviewTab,
+ functions: {
+ setCurrentDiscussionId: this.setCurrentDiscussionId,
+ getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
+ },
+ });
+ },
},
};
</script>
@@ -122,33 +152,35 @@ export default {
@mouseleave="handleMouseLeave(discussion)"
>
<template v-if="shouldGroupReplies">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line || diffLine"
- :discussion-file="discussion.diff_file"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="userCanReply"
- :discussion-root="true"
- :discussion-resolve-path="discussion.resolve_path"
- :is-overview-tab="isOverviewTab"
- @handleDeleteNote="$emit('deleteNote')"
- @startReplying="$emit('startReplying')"
- >
- <template #discussion-resolved-text>
- <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 discussion-resolved-text"
- />
- </template>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </component>
+ <gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line || diffLine"
+ :discussion-file="discussion.diff_file"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ :discussion-root="true"
+ :discussion-resolve-path="discussion.resolve_path"
+ :is-overview-tab="isOverviewTab"
+ @handleDeleteNote="$emit('deleteNote')"
+ @startReplying="$emit('startReplying')"
+ >
+ <template #discussion-resolved-text>
+ <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 discussion-resolved-text"
+ />
+ </template>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
+ </component>
+ </gl-intersection-observer>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
v-if="hasReplies"
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index 6ad565567be..1633b79c3be 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
export default {
@@ -27,13 +27,12 @@ export default {
};
},
computed: {
- ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }),
lineNumber() {
return this.commentLineOptions[this.commentLineOptions.length - 1].text;
},
},
created() {
- const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line;
+ const line = this.lineRange?.start || this.line;
this.commentLineStart = {
line_code: line.line_code,
@@ -42,7 +41,6 @@ export default {
new_line: line.new_line,
};
- if (this.selectedCommentPosition) return;
this.highlightSelection();
},
destroyed() {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 1ce1696e332..c09582d6287 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -19,6 +20,9 @@ export default {
noteForm,
Suggestions,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
mixins: [autosave],
props: {
note: {
@@ -144,6 +148,9 @@ export default {
this.removeSuggestionInfoFromBatch(suggestionId);
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji'],
+ },
};
</script>
@@ -163,11 +170,7 @@ export default {
@addToBatch="addSuggestionToBatch"
@removeFromBatch="removeSuggestionFromBatch"
/>
- <div
- v-else
- class="note-text md"
- v-html="note.note_html /* eslint-disable-line vue/no-v-html */"
- ></div>
+ <div v-else v-safe-html:[$options.safeHtmlConfig]="note.note_html" class="note-text md"></div>
<note-form
v-if="isEditing"
ref="noteForm"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index b05643e5e13..d6b65ed0e8b 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,9 +1,9 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
@@ -17,6 +17,8 @@ export default {
markdownField,
CommentFieldLayout,
GlButton,
+ GlSprintf,
+ GlLink,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: {
@@ -203,16 +205,12 @@ export default {
);
},
changedCommentText() {
- return sprintf(
- __(
+ return {
+ text: __(
'This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
),
- {
- startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`,
- endTag: '</a>',
- },
- false,
- );
+ placeholder: { link: ['startTag', 'endTag'] },
+ };
},
},
watch: {
@@ -318,11 +316,13 @@ export default {
<template>
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
- <div
- v-if="conflictWhileEditing"
- class="js-conflict-edit-warning alert alert-danger"
- v-html="changedCommentText /* eslint-disable-line vue/no-v-html */"
- ></div>
+ <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
+ <gl-sprintf :message="changedCommentText.text" :placeholders="changedCommentText.placeholder">
+ <template #link="{ content }">
+ <gl-link :href="noteHash" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
<comment-field-layout
@@ -334,13 +334,13 @@ export default {
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:line="line"
+ :lines="lines"
:note="discussionNote"
:can-suggest="canSuggest"
:add-spacing-classes="false"
:help-page-path="helpPagePath"
:show-suggest-popover="showSuggestPopover"
:textarea-value="updatedNoteBody"
- :lines="lines"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<template #textarea>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 58570e76795..3ab3e7a20d4 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -8,6 +8,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import draftNote from '../../batch_comments/components/draft_note.vue';
+import { discussionIntersectionObserverHandlerFactory } from '../../diffs/utils/discussions';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
@@ -38,6 +39,9 @@ export default {
TimelineEntryItem,
},
mixins: [glFeatureFlagsMixin()],
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
props: {
noteableData: {
type: Object,
@@ -94,15 +98,17 @@ export default {
return this.noteableData.noteableType;
},
allDiscussions() {
+ let skeletonNotes = [];
+
if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
- return new Array(prerenderedNotesCount).fill({
+ skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true,
});
}
- return this.discussions;
+ return this.discussions.concat(skeletonNotes);
},
canReply() {
return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled;
@@ -258,7 +264,13 @@ export default {
getFetchDiscussionsConfig() {
const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
- if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) {
+ const currentFilter =
+ this.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
+
+ if (
+ doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
+ currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
+ ) {
return {
...defaultConfig,
filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 96974c4fa2d..ad529eb99b6 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,7 +1,10 @@
import { mapGetters, mapActions, mapState } from 'vuex';
import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_utils';
+import { updateHistory } from '../../lib/utils/url_utility';
import eventHub from '../event_hub';
+const isDiffsVirtualScrollingEnabled = () => window.gon?.features?.diffsVirtualScrolling;
+
/**
* @param {string} selector
* @returns {boolean}
@@ -11,20 +14,52 @@ function scrollTo(selector, { withoutContext = false } = {}) {
const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
if (el) {
- scrollFunction(el);
+ scrollFunction(el, {
+ behavior: isDiffsVirtualScrollingEnabled() ? 'auto' : 'smooth',
+ });
return true;
}
return false;
}
+function updateUrlWithNoteId(noteId) {
+ const newHistoryEntry = {
+ state: null,
+ title: window.title,
+ url: `#note_${noteId}`,
+ replace: true,
+ };
+
+ if (noteId && isDiffsVirtualScrollingEnabled()) {
+ // Temporarily mask the ID to avoid the browser default
+ // scrolling taking over which is broken with virtual
+ // scrolling enabled.
+ const note = document.querySelector(`#note_${noteId}`);
+ note?.setAttribute('id', `masked::${note.id}`);
+
+ // Update the hash now that the ID "doesn't exist" in the page
+ updateHistory(newHistoryEntry);
+
+ // Unmask the note's ID
+ note?.setAttribute('id', `note_${noteId}`);
+ } else if (noteId) {
+ updateHistory(newHistoryEntry);
+ }
+}
+
/**
* @param {object} self Component instance with mixin applied
* @param {string} id Discussion id we are jumping to
*/
-function diffsJump({ expandDiscussion }, id) {
+function diffsJump({ expandDiscussion }, id, firstNoteId) {
const selector = `ul.notes[data-discussion-id="${id}"]`;
- eventHub.$once('scrollToDiscussion', () => scrollTo(selector));
+
+ eventHub.$once('scrollToDiscussion', () => {
+ scrollTo(selector);
+ // Wait for the discussion scroll before updating to the more specific ID
+ setTimeout(() => updateUrlWithNoteId(firstNoteId), 0);
+ });
expandDiscussion({ discussionId: id });
}
@@ -56,12 +91,13 @@ function switchToDiscussionsTabAndJumpTo(self, id) {
* @param {object} discussion Discussion we are jumping to
*/
function jumpToDiscussion(self, discussion) {
- const { id, diff_discussion: isDiffDiscussion } = discussion;
+ const { id, diff_discussion: isDiffDiscussion, notes } = discussion;
+ const firstNoteId = notes?.[0]?.id;
if (id) {
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'diffs' && isDiffDiscussion) {
- diffsJump(self, id);
+ diffsJump(self, id, firstNoteId);
} else if (activeTab === 'show') {
discussionJump(self, id);
} else {
@@ -79,10 +115,18 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId)
const isDiffView = window.mrTabs.currentAction === 'diffs';
const targetId = fn(discussionId, isDiffView);
const discussion = self.getDiscussion(targetId);
+ const setHash = !isDiffView && !isDiffsVirtualScrollingEnabled();
const discussionFilePath = discussion?.diff_file?.file_path;
+ if (isDiffsVirtualScrollingEnabled()) {
+ window.location.hash = '';
+ }
+
if (discussionFilePath) {
- self.scrollToFile(discussionFilePath);
+ self.scrollToFile({
+ path: discussionFilePath,
+ setHash,
+ });
}
self.$nextTick(() => {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7eb10f647a0..c862a29ad9c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
@@ -71,7 +70,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, discussions) =>
- commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, discussions);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
@@ -90,14 +89,51 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
+ if (window.gon?.features?.paginatedIssueDiscussions) {
+ return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
+ }
+
return axios.get(path, config).then(({ data }) => {
- commit(types.SET_INITIAL_DISCUSSIONS, data);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
});
};
+export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
+ const params = { ...config?.params, per_page: perPage };
+
+ if (cursor) {
+ params.cursor = cursor;
+ }
+
+ return axios.get(path, { params }).then(({ data, headers }) => {
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
+
+ if (headers['x-next-page-cursor']) {
+ const nextConfig = { ...config };
+
+ if (config?.params?.persist_filter) {
+ delete nextConfig.params.notes_filter;
+ delete nextConfig.params.persist_filter;
+ }
+
+ return dispatch('fetchDiscussionsBatch', {
+ path,
+ config: nextConfig,
+ cursor: headers['x-next-page-cursor'],
+ perPage: Math.min(Math.round(perPage * 1.5), 100),
+ });
+ }
+
+ commit(types.SET_FETCHING_DISCUSSIONS, false);
+ dispatch('updateResolvableDiscussionsCounts');
+
+ return undefined;
+ });
+};
+
export const updateDiscussion = ({ commit, state }, discussion) => {
commit(types.UPDATE_DISCUSSION, discussion);
@@ -621,7 +657,7 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
createFlash({
- message: __(flashMessage),
+ message: flashMessage,
parent: flashContainer,
});
})
@@ -657,7 +693,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
const flashMessage = errorMessage || defaultMessage;
createFlash({
- message: __(flashMessage),
+ message: flashMessage,
parent: flashContainer,
});
})
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2e8b728e013..fcd2846ff0d 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,11 +1,11 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
+export const ADD_OR_UPDATE_DISCUSSIONS = 'ADD_OR_UPDATE_DISCUSSIONS';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c5fa34dfedd..1a99750ddb3 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -129,8 +129,8 @@ export default {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
- const discussions = discussionsData.reduce((acc, d) => {
+ [types.ADD_OR_UPDATE_DISCUSSIONS](state, discussionsData) {
+ discussionsData.forEach((d) => {
const discussion = { ...d };
const diffData = {};
@@ -145,27 +145,38 @@ export default {
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach((n) => {
- acc.push({
+ const newDiscussion = {
...discussion,
...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note
- });
+ };
+ const oldDiscussion = state.discussions.find(
+ (existingDiscussion) =>
+ existingDiscussion.id === discussion.id && existingDiscussion.notes[0].id === n.id,
+ );
+
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, newDiscussion);
+ } else {
+ state.discussions.push(newDiscussion);
+ }
});
} else {
- const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
+ const oldDiscussion = utils.findNoteObjectById(state.discussions, discussion.id);
- acc.push({
- ...discussion,
- ...diffData,
- expanded: oldNote ? oldNote.expanded : discussion.expanded,
- });
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, {
+ ...discussion,
+ ...diffData,
+ expanded: oldDiscussion.expanded,
+ });
+ } else {
+ state.discussions.push({ ...discussion, ...diffData });
+ }
}
-
- return acc;
- }, []);
-
- Object.assign(state, { discussions });
+ });
},
+
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
diff --git a/app/assets/javascripts/packages/list/components/package_search.vue b/app/assets/javascripts/packages/list/components/package_search.vue
deleted file mode 100644
index 869a2c2f641..00000000000
--- a/app/assets/javascripts/packages/list/components/package_search.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { mapState, mapActions } from 'vuex';
-import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import { sortableFields } from '../utils';
-import PackageTypeToken from './tokens/package_type_token.vue';
-
-export default {
- tokens: [
- {
- type: 'type',
- icon: 'package',
- title: s__('PackageRegistry|Type'),
- unique: true,
- token: PackageTypeToken,
- operators: OPERATOR_IS_ONLY,
- },
- ],
- components: { RegistrySearch, UrlSync },
- computed: {
- ...mapState({
- isGroupPage: (state) => state.config.isGroupPage,
- sorting: (state) => state.sorting,
- filter: (state) => state.filter,
- }),
- sortableFields() {
- return sortableFields(this.isGroupPage);
- },
- },
- methods: {
- ...mapActions(['setSorting', 'setFilter']),
- updateSorting(newValue) {
- this.setSorting(newValue);
- this.$emit('update');
- },
- },
-};
-</script>
-
-<template>
- <url-sync>
- <template #default="{ updateQuery }">
- <registry-search
- :filter="filter"
- :sorting="sorting"
- :tokens="$options.tokens"
- :sortable-fields="sortableFields"
- @sorting:changed="updateSorting"
- @filter:changed="setFilter"
- @filter:submit="$emit('update')"
- @query:changed="updateQuery"
- />
- </template>
- </url-sync>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/package_title.vue b/app/assets/javascripts/packages/list/components/package_title.vue
deleted file mode 100644
index 426ad150ea9..00000000000
--- a/app/assets/javascripts/packages/list/components/package_title.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { n__ } from '~/locale';
-import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '../constants';
-
-export default {
- name: 'PackageTitle',
- components: {
- TitleArea,
- MetadataItem,
- },
- props: {
- count: {
- type: Number,
- required: false,
- default: null,
- },
- helpUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- showPackageCount() {
- return Number.isInteger(this.count);
- },
- packageAmountText() {
- return n__(`%d Package`, `%d Packages`, this.count);
- },
- infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
- },
- },
- i18n: {
- LIST_TITLE_TEXT,
- },
-};
-</script>
-
-<template>
- <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
- <template #metadata-amount>
- <metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" />
- </template>
- </title-area>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index 4c5fb0ee7c9..31d90fa4dee 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -7,6 +7,8 @@ import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue';
+import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
import PackageList from './packages_list.vue';
@@ -16,28 +18,10 @@ export default {
GlLink,
GlSprintf,
PackageList,
- PackageTitle: () =>
- import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'),
- PackageSearch: () =>
- import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'),
- InfrastructureTitle: () =>
- import(
- /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'
- ),
- InfrastructureSearch: () =>
- import(
- /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'
- ),
+ InfrastructureTitle,
+ InfrastructureSearch,
},
inject: {
- titleComponent: {
- from: 'titleComponent',
- default: 'PackageTitle',
- },
- searchComponent: {
- from: 'searchComponent',
- default: 'PackageSearch',
- },
emptyPageTitle: {
from: 'emptyPageTitle',
default: s__('PackageRegistry|There are no packages yet'),
@@ -111,8 +95,8 @@ export default {
<template>
<div>
- <component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" />
- <component :is="searchComponent" @update="requestPackagesList" />
+ <infrastructure-title :help-url="packageHelpUrl" :count="packagesCount" />
+ <infrastructure-search @update="requestPackagesList" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
diff --git a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
deleted file mode 100644
index 74b6774712e..00000000000
--- a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PACKAGE_TYPES } from '../../constants';
-
-export default {
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- },
- PACKAGE_TYPES,
-};
-</script>
-
-<template>
- <gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
- <template #suggestions>
- <gl-filtered-search-suggestion
- v-for="(type, index) in $options.PACKAGE_TYPES"
- :key="index"
- :value="type.type"
- >
- {{ type.title }}
- </gl-filtered-search-suggestion>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 2c6fd94024e..4f5071e784b 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -96,10 +96,4 @@ export const PACKAGE_TYPES = [
},
];
-export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry');
-
-export const LIST_INTRO_TEXT = s__(
- 'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
-);
-
export const TERRAFORM_SEARCH_TYPE = Object.freeze({ value: { data: 'terraform_module' } });
diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue
index e4a1a1a8266..e4a1a1a8266 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_button.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue
diff --git a/app/assets/javascripts/registry/explorer/components/delete_image.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue
index a313854f5e4..a313854f5e4 100644
--- a/app/assets/javascripts/registry/explorer/components/delete_image.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_image.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
index 56d2ff86fb7..56d2ff86fb7 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
index f857c96c9d1..f857c96c9d1 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index e9e36151fe6..e9e36151fe6 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
index a16d95a6b30..a16d95a6b30 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue
index 12095655126..12095655126 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue
index fc1504f6c31..fc1504f6c31 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 3e19a646f53..3e19a646f53 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
index 0556fd298aa..0556fd298aa 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue
index b7afa5fba33..b7afa5fba33 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
index 1f52e319ad0..1f52e319ad0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue
index 07ee3c6083b..07ee3c6083b 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
index a68c4de5aa6..a68c4de5aa6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
index 5bd13322ebb..5bd13322ebb 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
index c1ec523574a..c1ec523574a 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
index 5aa04419ca0..5aa04419ca0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
index 6d2ff9ea7b6..6d2ff9ea7b6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue
index e77eda31596..e77eda31596 100644
--- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue
diff --git a/app/assets/javascripts/registry/explorer/constants/common.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
index f7beec2c935..f7beec2c935 100644
--- a/app/assets/javascripts/registry/explorer/constants/common.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 19e1a75fb2f..19e1a75fb2f 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
diff --git a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
index 40f9b09a982..40f9b09a982 100644
--- a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js
diff --git a/app/assets/javascripts/registry/explorer/constants/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js
index 6886356d8e2..6886356d8e2 100644
--- a/app/assets/javascripts/registry/explorer/constants/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/index.js
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index d21a154d1b8..d21a154d1b8 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
diff --git a/app/assets/javascripts/registry/explorer/constants/quick_start.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js
index 6a39c07eba2..6a39c07eba2 100644
--- a/app/assets/javascripts/registry/explorer/constants/quick_start.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/quick_start.js
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
new file mode 100644
index 00000000000..9694bfd4e77
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ batchMax: 1,
+ },
+ ),
+});
diff --git a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
index 4c88b726ee5..4c88b726ee5 100644
--- a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
index a31f2829e13..a31f2829e13 100644
--- a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 01cb7fa1cab..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index b5a99fd9ac1..b5a99fd9ac1 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index a703c2dd0ac..a703c2dd0ac 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
index 9092a71edb0..9092a71edb0 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index 246a6768593..246a6768593 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index feabc4f770b..feabc4f770b 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue
index dca63e1a569..dca63e1a569 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/index.vue
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index 73b957f42f2..73b957f42f2 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js
index a0c4417d549..a0c4417d549 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/router.js
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 73fb3656af1..71e8cf4f634 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -1,32 +1,49 @@
<script>
-import { GlAlert, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
-import { __ } from '~/locale';
+import {
+ GlAlert,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlSkeletonLoader,
+ GlSprintf,
+ GlEmptyState,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
export default {
components: {
- GlFormGroup,
GlAlert,
+ GlEmptyState,
+ GlFormGroup,
GlFormInputGroup,
+ GlSkeletonLoader,
GlSprintf,
ClipboardButton,
TitleArea,
- GlSkeletonLoader,
+ ManifestsList,
},
- inject: ['groupPath', 'dependencyProxyAvailable'],
+ inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'],
i18n: {
- proxyNotAvailableText: __('Dependency Proxy feature is limited to public groups for now.'),
- proxyDisabledText: __('Dependency Proxy disabled. To enable it, contact the group owner.'),
- proxyImagePrefix: __('Dependency Proxy image prefix'),
- copyImagePrefixText: __('Copy prefix'),
- blobCountAndSize: __('Contains %{count} blobs of images (%{size})'),
+ proxyNotAvailableText: s__(
+ 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
+ ),
+ proxyDisabledText: s__(
+ 'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.',
+ ),
+ proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
+ copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
+ blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
+ pageTitle: s__('DependencyProxy|Dependency Proxy'),
+ noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
},
data() {
return {
@@ -40,7 +57,7 @@ export default {
return !this.dependencyProxyAvailable;
},
variables() {
- return { fullPath: this.groupPath };
+ return this.queryVariables;
},
},
},
@@ -56,13 +73,45 @@ export default {
dependencyProxyEnabled() {
return this.group?.dependencyProxySetting?.enabled;
},
+ queryVariables() {
+ return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
+ },
+ pageInfo() {
+ return this.group.dependencyProxyManifests.pageInfo;
+ },
+ manifests() {
+ return this.group.dependencyProxyManifests.nodes;
+ },
+ },
+ methods: {
+ fetchNextPage() {
+ this.fetchMore({
+ first: GRAPHQL_PAGE_SIZE,
+ after: this.pageInfo?.endCursor,
+ });
+ },
+ fetchPreviousPage() {
+ this.fetchMore({
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ });
+ },
+ fetchMore(variables) {
+ this.$apollo.queries.group.fetchMore({
+ variables: { ...this.queryVariables, ...variables },
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ },
},
};
</script>
<template>
<div>
- <title-area :title="__('Dependency Proxy')" :info-messages="infoMessages" />
+ <title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages" />
<gl-alert
v-if="!dependencyProxyAvailable"
:dismissible="false"
@@ -97,6 +146,20 @@ export default {
</span>
</template>
</gl-form-group>
+
+ <manifests-list
+ v-if="manifests && manifests.length"
+ :manifests="manifests"
+ :pagination="pageInfo"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ />
+
+ <gl-empty-state
+ v-else
+ :svg-path="noManifestsIllustration"
+ :title="$options.i18n.noManifestTitle"
+ />
</div>
<gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
{{ $options.i18n.proxyDisabledText }}
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
new file mode 100644
index 00000000000..78880b6e3f4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'ManifestRow',
+ components: {
+ GlSprintf,
+ ListItem,
+ TimeagoTooltip,
+ },
+ props: {
+ manifest: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ name() {
+ return this.manifest?.imageName.split(':')[0];
+ },
+ version() {
+ return this.manifest?.imageName.split(':')[1];
+ },
+ },
+ i18n: {
+ cachedAgoMessage: s__('DependencyProxy|Cached %{time}'),
+ },
+};
+</script>
+
+<template>
+ <list-item>
+ <template #left-primary> {{ name }} </template>
+ <template #left-secondary> {{ version }} </template>
+ <template #right-primary> &nbsp; </template>
+ <template #right-secondary>
+ <timeago-tooltip :time="manifest.createdAt" data-testid="cached-message">
+ <template #default="{ timeAgo }">
+ <gl-sprintf :message="$options.i18n.cachedAgoMessage">
+ <template #time>{{ timeAgo }}</template>
+ </gl-sprintf>
+ </template>
+ </timeago-tooltip>
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue
new file mode 100644
index 00000000000..005c8feea3a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+
+export default {
+ name: 'ManifestsLists',
+ components: {
+ ManifestRow,
+ GlKeysetPagination,
+ },
+ props: {
+ manifests: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ listTitle: s__('DependencyProxy|Image list'),
+ },
+ computed: {
+ showPagination() {
+ return this.pagination.hasNextPage || this.pagination.hasPreviousPage;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-6">
+ <h3 class="gl-font-base">{{ $options.i18n.listTitle }}</h3>
+ <div
+ class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column"
+ >
+ <manifest-row v-for="(manifest, index) in manifests" :key="index" :manifest="manifest" />
+ </div>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pagination"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
new file mode 100644
index 00000000000..3c6ede6fdce
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
@@ -0,0 +1 @@
+export const GRAPHQL_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
index 9058d349bf3..63d5469c955 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql
@@ -1,4 +1,12 @@
-query getDependencyProxyDetails($fullPath: ID!) {
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getDependencyProxyDetails(
+ $fullPath: ID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
group(fullPath: $fullPath) {
dependencyProxyBlobCount
dependencyProxyTotalSize
@@ -6,5 +14,14 @@ query getDependencyProxyDetails($fullPath: ID!) {
dependencyProxySetting {
enabled
}
+ dependencyProxyManifests(after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ createdAt
+ imageName
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
}
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index 3d3fa62fd43..bcbeec72961 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -23,6 +23,7 @@ import PackageFiles from '~/packages_and_registries/package_registry/components/
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_COMPOSER,
@@ -35,12 +36,10 @@ import {
CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking';
@@ -62,6 +61,7 @@ export default {
AdditionalMetadata,
InstallationCommands,
PackageFiles,
+ DeletePackage,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -148,40 +148,15 @@ export default {
formatSize(size) {
return numberToHumanSize(size);
},
- async deletePackage() {
- const { data } = await this.$apollo.mutate({
- mutation: destroyPackageMutation,
- variables: {
- id: this.packageEntity.id,
- },
- });
+ navigateToListWithSuccessModal() {
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
- if (data?.destroyPackage?.errors[0]) {
- throw data.destroyPackage.errors[0];
- }
- },
- async confirmPackageDeletion() {
- this.track(DELETE_PACKAGE_TRACKING_ACTION);
-
- try {
- await this.deletePackage();
-
- const returnTo =
- !this.groupListUrl || document.referrer.includes(this.projectName)
- ? this.projectListUrl
- : this.groupListUrl; // to avoid security issue url are supplied from backend
-
- const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
- window.location.replace(`${returnTo}?${modalQuery}`);
- } catch (error) {
- createFlash({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- type: 'warning',
- captureError: true,
- error,
- });
- }
+ window.location.replace(`${returnTo}?${modalQuery}`);
},
async deletePackageFile(id) {
try {
@@ -322,26 +297,33 @@ export default {
</gl-tab>
</gl-tabs>
- <gl-modal
- ref="deleteModal"
- modal-id="delete-modal"
- data-testid="delete-modal"
- :action-primary="$options.modal.packageDeletePrimaryAction"
- :action-cancel="$options.modal.cancelAction"
- @primary="confirmPackageDeletion"
- @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
+ <delete-package
+ @start="track($options.trackingActions.DELETE_PACKAGE_TRACKING_ACTION)"
+ @end="navigateToListWithSuccessModal"
>
- <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
- <template #version>
- <strong>{{ packageEntity.version }}</strong>
- </template>
+ <template #default="{ deletePackage }">
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-modal"
+ data-testid="delete-modal"
+ :action-primary="$options.modal.packageDeletePrimaryAction"
+ :action-cancel="$options.modal.cancelAction"
+ @primary="deletePackage(packageEntity)"
+ @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
+ >
+ <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
- <template #name>
- <strong>{{ packageEntity.name }}</strong>
- </template>
- </gl-sprintf>
- </gl-modal>
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ </template>
+ </delete-package>
<gl-modal
ref="deleteFileModal"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
new file mode 100644
index 00000000000..7a85fd3052e
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
@@ -0,0 +1,62 @@
+<script>
+import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/package_registry/constants';
+
+export default {
+ props: {
+ refetchQueries: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ showSuccessAlert: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ errorMessage: s__('PackageRegistry|Something went wrong while deleting the package.'),
+ successMessage: DELETE_PACKAGE_SUCCESS_MESSAGE,
+ },
+ methods: {
+ async deletePackage(packageEntity) {
+ try {
+ this.$emit('start');
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackageMutation,
+ variables: {
+ id: packageEntity.id,
+ },
+ awaitRefetchQueries: Boolean(this.refetchQueries),
+ refetchQueries: this.refetchQueries,
+ });
+
+ if (data?.destroyPackage?.errors[0]) {
+ throw data.destroyPackage.errors[0];
+ }
+ if (this.showSuccessAlert) {
+ createFlash({
+ message: this.$options.i18n.successMessage,
+ type: 'success',
+ });
+ }
+ } catch (error) {
+ createFlash({
+ message: this.$options.i18n.errorMessage,
+ type: 'warning',
+ captureError: true,
+ error,
+ });
+ }
+ this.$emit('end');
+ },
+ },
+ render() {
+ return this.$scopedSlots.default({ deletePackage: this.deletePackage });
+ },
+};
+</script>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
index 08481ac5655..11eeaf933ff 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue
@@ -1,33 +1,31 @@
<script>
-/*
- * The following component has several commented lines, this is because we are refactoring them piece by piece on several mrs
- * For a complete overview of the plan please check: https://gitlab.com/gitlab-org/gitlab/-/issues/330846
- * This work is behind feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/341136
- */
-// import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
- LIST_QUERY_DEBOUNCE_TIME,
+ GRAPHQL_PAGE_SIZE,
+ DELETE_PACKAGE_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
+import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import PackageTitle from './package_title.vue';
import PackageSearch from './package_search.vue';
-// import PackageList from './packages_list.vue';
+import PackageList from './packages_list.vue';
export default {
components: {
- // GlEmptyState,
- // GlLink,
- // GlSprintf,
- // PackageList,
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ PackageList,
PackageTitle,
PackageSearch,
+ DeletePackage,
},
inject: [
'packageHelpUrl',
@@ -41,6 +39,7 @@ export default {
packages: {},
sort: '',
filters: {},
+ mutationLoading: false,
};
},
apollo: {
@@ -52,7 +51,9 @@ export default {
update(data) {
return data[this.graphqlResource].packages;
},
- debounce: LIST_QUERY_DEBOUNCE_TIME,
+ skip() {
+ return !this.sort;
+ },
},
},
computed: {
@@ -64,22 +65,40 @@ export default {
groupSort: this.isGroupPage ? this.sort : undefined,
packageName: this.filters?.packageName,
packageType: this.filters?.packageType,
+ first: GRAPHQL_PAGE_SIZE,
};
},
graphqlResource() {
return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE;
},
+ pageInfo() {
+ return this.packages?.pageInfo ?? {};
+ },
packagesCount() {
return this.packages?.count;
},
hasFilters() {
return this.filters.packageName && this.filters.packageType;
},
+ emptySearch() {
+ return !this.filters.packageName && !this.filters.packageType;
+ },
emptyStateTitle() {
return this.emptySearch
? this.$options.i18n.emptyPageTitle
: this.$options.i18n.noResultsTitle;
},
+ isLoading() {
+ return this.$apollo.queries.packages.loading || this.mutationLoading;
+ },
+ refetchQueriesData() {
+ return [
+ {
+ query: getPackagesQuery,
+ variables: this.queryVariables,
+ },
+ ];
+ },
},
mounted() {
this.checkDeleteAlert();
@@ -99,6 +118,35 @@ export default {
this.sort = sort;
this.filters = { ...filters };
},
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ fetchNextPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.pageInfo?.endCursor,
+ };
+
+ this.$apollo.queries.packages.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
+ fetchPreviousPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ };
+
+ this.$apollo.queries.packages.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@@ -116,19 +164,35 @@ export default {
<package-title :help-url="packageHelpUrl" :count="packagesCount" />
<package-search @update="handleSearchUpdate" />
- <!-- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
- <template #empty-state>
- <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
- <template #description>
- <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
- <gl-sprintf v-else :message="$options.i18n.noResultsText">
- <template #noPackagesLink="{ content }">
- <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ <delete-package
+ :refetch-queries="refetchQueriesData"
+ show-success-alert
+ @start="mutationLoading = true"
+ @end="mutationLoading = false"
+ >
+ <template #default="{ deletePackage }">
+ <package-list
+ :list="packages.nodes"
+ :is-loading="isLoading"
+ :page-info="pageInfo"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ @package:delete="deletePackage"
+ >
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="$options.i18n.noResultsText">
+ <template #noPackagesLink="{ content }">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
- </gl-sprintf>
+ </gl-empty-state>
</template>
- </gl-empty-state>
+ </package-list>
</template>
- </package-list> -->
+ </delete-package>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 836df59ca58..3483d23e251 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import { sortableFields } from '~/packages/list/utils';
+import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
index 6e00a48586e..bf41c36e09b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
@@ -1,6 +1,5 @@
<script>
-import { n__ } from '~/locale';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
+import { n__, s__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -29,11 +28,14 @@ export default {
return n__(`%d Package`, `%d Packages`, this.count);
},
infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
+ return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }];
},
},
i18n: {
- LIST_TITLE_TEXT,
+ LIST_TITLE_TEXT: s__('PackageRegistry|Package Registry'),
+ LIST_INTRO_TEXT: s__(
+ 'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
+ ),
},
};
</script>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index 25bac687dbf..2a946544c2f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -1,82 +1,94 @@
<script>
-import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
-import { mapState, mapGetters } from 'vuex';
+import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
import { s__ } from '~/locale';
-import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
-import { TrackingActions } from '~/packages/shared/constants';
-import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
+import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
import Tracking from '~/tracking';
export default {
components: {
- GlPagination,
+ GlKeysetPagination,
GlModal,
GlSprintf,
PackagesListLoader,
PackagesListRow,
},
mixins: [Tracking.mixin()],
+ props: {
+ list: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+
data() {
return {
itemToBeDeleted: null,
};
},
computed: {
- ...mapState({
- perPage: (state) => state.pagination.perPage,
- totalItems: (state) => state.pagination.total,
- page: (state) => state.pagination.page,
- isGroupPage: (state) => state.config.isGroupPage,
- isLoading: 'isLoading',
- }),
- ...mapGetters({ list: 'getList' }),
- currentPage: {
- get() {
- return this.page;
- },
- set(value) {
- this.$emit('page:changed', value);
- },
- },
isListEmpty() {
return !this.list || this.list.length === 0;
},
- modalAction() {
- return s__('PackageRegistry|Delete package');
- },
deletePackageName() {
return this.itemToBeDeleted?.name ?? '';
},
tracking() {
const category = this.itemToBeDeleted
- ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
+ ? packageTypeToTrackCategory(this.itemToBeDeleted.packageType)
: undefined;
return {
category,
};
},
+ showPagination() {
+ return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
+ },
+ showDeleteModal: {
+ get() {
+ return Boolean(this.itemToBeDeleted);
+ },
+ set(value) {
+ if (!value) {
+ this.itemToBeDeleted = null;
+ }
+ },
+ },
},
methods: {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
- this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
- this.$refs.packageListDeleteModal.show();
+ this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION);
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
- this.track(TrackingActions.DELETE_PACKAGE);
- this.itemToBeDeleted = null;
+ this.track(DELETE_PACKAGE_TRACKING_ACTION);
},
deleteItemCanceled() {
- this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
- this.itemToBeDeleted = null;
+ this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION);
},
},
i18n: {
deleteModalContent: s__(
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
),
+ modalAction: s__('PackageRegistry|Delete package'),
},
};
</script>
@@ -95,29 +107,29 @@ export default {
v-for="packageEntity in list"
:key="packageEntity.id"
:package-entity="packageEntity"
- :package-link="packageEntity._links.web_path"
- :is-group="isGroupPage"
@packageToDelete="setItemToBeDeleted"
/>
</div>
- <gl-pagination
- v-model="currentPage"
- :per-page="perPage"
- :total-items="totalItems"
- align="center"
- class="gl-w-full gl-mt-3"
- />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pageInfo"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
<gl-modal
- ref="packageListDeleteModal"
+ v-model="showDeleteModal"
modal-id="confirm-delete-pacakge"
ok-variant="danger"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-ok>{{ $options.i18n.modalAction }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
index 529a7893dfc..59354e77ee9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PACKAGE_TYPES } from '~/packages/list/constants';
+import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/constants';
export default {
components: {
@@ -17,9 +17,9 @@ export default {
<gl-filtered-search-suggestion
v-for="(type, index) in $options.PACKAGE_TYPES"
:key="index"
- :value="type.type"
+ :value="type"
>
- {{ type.title }}
+ {{ type }}
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 6a88880fa90..9fd8880861c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
@@ -59,16 +59,7 @@ export const TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND =
export const TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND =
'copy_composer_package_include_command';
-export const TrackingCategories = {
- [PACKAGE_TYPE_MAVEN]: 'MavenPackages',
- [PACKAGE_TYPE_NPM]: 'NpmPackages',
- [PACKAGE_TYPE_CONAN]: 'ConanPackages',
-};
-
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
-export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package.',
-);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package file.',
);
@@ -79,6 +70,8 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
'PackageRegistry|Failed to load the package data',
);
+export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
+
export const PACKAGE_ERROR_STATUS = 'ERROR';
export const PACKAGE_DEFAULT_STATUS = 'DEFAULT';
export const PACKAGE_HIDDEN_STATUS = 'HIDDEN';
@@ -92,4 +85,52 @@ export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
export const PROJECT_RESOURCE_TYPE = 'project';
export const GROUP_RESOURCE_TYPE = 'group';
-export const LIST_QUERY_DEBOUNCE_TIME = 50;
+export const GRAPHQL_PAGE_SIZE = 20;
+
+export const LIST_KEY_NAME = 'name';
+export const LIST_KEY_PROJECT = 'project_path';
+export const LIST_KEY_VERSION = 'version';
+export const LIST_KEY_PACKAGE_TYPE = 'type';
+export const LIST_KEY_CREATED_AT = 'created_at';
+
+export const LIST_LABEL_NAME = __('Name');
+export const LIST_LABEL_PROJECT = __('Project');
+export const LIST_LABEL_VERSION = __('Version');
+export const LIST_LABEL_PACKAGE_TYPE = __('Type');
+export const LIST_LABEL_CREATED_AT = __('Published');
+
+export const SORT_FIELDS = [
+ {
+ orderBy: LIST_KEY_NAME,
+ label: LIST_LABEL_NAME,
+ },
+ {
+ orderBy: LIST_KEY_PROJECT,
+ label: LIST_LABEL_PROJECT,
+ },
+ {
+ orderBy: LIST_KEY_VERSION,
+ label: LIST_LABEL_VERSION,
+ },
+ {
+ orderBy: LIST_KEY_PACKAGE_TYPE,
+ label: LIST_LABEL_PACKAGE_TYPE,
+ },
+ {
+ orderBy: LIST_KEY_CREATED_AT,
+ label: LIST_LABEL_CREATED_AT,
+ },
+];
+
+export const PACKAGE_TYPES = [
+ s__('PackageRegistry|Composer'),
+ s__('PackageRegistry|Conan'),
+ s__('PackageRegistry|Generic'),
+ s__('PackageRegistry|Maven'),
+ s__('PackageRegistry|npm'),
+ s__('PackageRegistry|NuGet'),
+ s__('PackageRegistry|PyPI'),
+ s__('PackageRegistry|RubyGems'),
+ s__('PackageRegistry|Debian'),
+ s__('PackageRegistry|Helm'),
+];
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
index f8cb5c516e2..21d6fbc9e1f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
@@ -17,7 +17,6 @@ export const apolloProvider = new VueApollo({
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
index 74e6de87866..e3115365f8b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
@@ -1,4 +1,5 @@
#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql"
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getPackages(
$fullPath: ID!
@@ -7,21 +8,47 @@ query getPackages(
$groupSort: PackageGroupSort
$packageName: String
$packageType: PackageTypeEnum
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- packages(sort: $sort, packageName: $packageName, packageType: $packageType) {
+ packages(
+ sort: $sort
+ packageName: $packageName
+ packageType: $packageType
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
count
nodes {
...PackageData
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) {
+ packages(
+ sort: $groupSort
+ packageName: $packageName
+ packageType: $packageType
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
count
nodes {
...PackageData
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/utils.js b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
index ae886952c3e..4ff8edb8f66 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
@@ -1,3 +1,4 @@
+import { capitalize } from 'lodash';
import { s__ } from '~/locale';
import {
PACKAGE_TYPE_CONAN,
@@ -10,6 +11,8 @@ import {
PACKAGE_TYPE_GENERIC,
PACKAGE_TYPE_DEBIAN,
PACKAGE_TYPE_HELM,
+ LIST_KEY_PROJECT,
+ SORT_FIELDS,
} from './constants';
export const getPackageTypeLabel = (packageType) => {
@@ -38,3 +41,8 @@ export const getPackageTypeLabel = (packageType) => {
return null;
}
};
+
+export const packageTypeToTrackCategory = (type) => `UI::${capitalize(type)}Packages`;
+
+export const sortableFields = (isGroupPage) =>
+ SORT_FIELDS.filter((f) => f.orderBy !== LIST_KEY_PROJECT || isGroupPage);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index 2dbe36def0e..5815c6393a7 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -103,6 +103,7 @@ export default {
:disabled="isLoading"
:label="$options.i18n.label"
data-qa-selector="dependency_proxy_setting_toggle"
+ data-testid="dependency-proxy-setting-toggle"
/>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
index 16152eb81f6..56f95fa2c1f 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
@@ -5,10 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/pages/admin/deploy_keys/index/index.js b/app/assets/javascripts/pages/admin/deploy_keys/index/index.js
new file mode 100644
index 00000000000..1e52aa3efd8
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/deploy_keys/index/index.js
@@ -0,0 +1,3 @@
+import { initAdminDeployKeysTable } from '~/admin/deploy_keys';
+
+initAdminDeployKeysTable();
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index a94a60af7ff..4cad87492cf 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,5 +1,5 @@
-import initDevOpsScore from '~/analytics/devops_report/devops_score';
-import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
+import initDevOpsScore from '~/analytics/devops_reports/devops_score';
+import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
initDevOpsScoreDisabledServicePing();
initDevOpsScore();
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 055d6f40c14..b06c804f3ca 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -46,7 +46,7 @@ export default {
return sprintf(
s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
- and all related resources, including issues and merge requests. Once you confirm and press
+ and all related resources, including issues and merge requests. After you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${escape(this.projectName)}</strong>`,
@@ -70,7 +70,7 @@ export default {
},
primaryProps() {
return {
- text: s__('Delete project'),
+ text: __('Delete project'),
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
};
},
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 946076cfb29..a1e7eb5d3de 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, no-unneeded-ternary */
+/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import { getGroups } from '~/api/groups_api';
@@ -78,7 +78,7 @@ export default class Todos {
initDeprecatedJQueryDropdown($dropdown, {
fieldName,
selectable: true,
- filterable: searchFields ? true : false,
+ filterable: Boolean(searchFields),
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: () => {
@@ -172,8 +172,8 @@ export default class Todos {
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
- document.querySelector('.todos-pending .badge').innerHTML = addDelimiter(data.count);
- document.querySelector('.todos-done .badge').innerHTML = addDelimiter(data.done_count);
+ document.querySelector('.js-todos-pending .badge').innerHTML = addDelimiter(data.count);
+ document.querySelector('.js-todos-done .badge').innerHTML = addDelimiter(data.done_count);
}
goToTodoUrl(e) {
diff --git a/app/assets/javascripts/pages/groups/crm/contacts/index.js b/app/assets/javascripts/pages/groups/crm/contacts/index.js
new file mode 100644
index 00000000000..a595246957f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/crm/contacts/index.js
@@ -0,0 +1,3 @@
+import initCrmContactsApp from '~/crm/contacts_bundle';
+
+initCrmContactsApp();
diff --git a/app/assets/javascripts/pages/groups/crm/organizations/index.js b/app/assets/javascripts/pages/groups/crm/organizations/index.js
new file mode 100644
index 00000000000..16479b43d52
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/crm/organizations/index.js
@@ -0,0 +1,3 @@
+import initCrmOrganizationsApp from '~/crm/organizations_bundle';
+
+initCrmOrganizationsApp();
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 49b9822795c..604da77f60c 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -10,10 +10,12 @@ import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import setupTransferEdit from '~/transfer_edit';
+import initConfirmDanger from '~/init_confirm_danger';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
initConfirmDangerModal();
+ initConfirmDanger();
initSettingsPanels();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue
index 9aac364d20e..c3ac074cd7a 100644
--- a/app/assets/javascripts/pages/groups/new/components/app.vue
+++ b/app/assets/javascripts/pages/groups/new/components/app.vue
@@ -47,7 +47,7 @@ export default {
<template>
<new-namespace-page
:jump-to-last-persisted-panel="hasErrors"
- :initial-breadcrumb="s__('New group')"
+ :initial-breadcrumb="__('New group')"
:panels="$options.PANELS"
:title="s__('GroupsNew|Create new group')"
persistence-key="new_group_last_active_tab"
diff --git a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
index ea08a0821a8..35193171fb8 100644
--- a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
+++ b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
@@ -20,7 +20,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.',
+ 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.',
)
"
>
diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
deleted file mode 100644
index 301e0b4f7a2..00000000000
--- a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { buildApiUrl } from '~/api/api_utils';
-import axios from '~/lib/utils/axios_utils';
-
-const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
-
-export default function fetchGroupPathAvailability(groupPath, parentId) {
- const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
-
- return axios.get(url, {
- params: { parent_id: parentId },
- });
-}
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index c58be202043..8ce73be6e74 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -3,7 +3,7 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
-import fetchGroupPathAvailability from './fetch_group_path_availability';
+import { getGroupPathAvailability } from '~/rest_api';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -45,7 +45,7 @@ export default class GroupPathValidator extends InputValidator {
if (inputDomElement.checkValidity() && groupPath.length > 1) {
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
- fetchGroupPathAvailability(groupPath, parentId)
+ getGroupPathAvailability(groupPath, parentId)
.then(({ data }) => data)
.then((data) => {
GroupPathValidator.setInputState(inputDomElement, !data.exists);
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
index 95522573b53..f9eecff4ac4 100644
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -1,10 +1,3 @@
-(async function packageApp() {
- if (window.gon.features.packageListApollo) {
- const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
+import packageList from '~/packages_and_registries/package_registry/pages/list';
- newPackageList.default();
- } else {
- const packageList = await import('~/packages/list/packages_list_app_bundle');
- packageList.default();
- }
-})();
+packageList();
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
index 6fd32321568..44579ee1217 100644
--- a/app/assets/javascripts/pages/groups/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -1,4 +1,4 @@
-import registryExplorer from '~/registry/explorer/index';
+import registryExplorer from '~/packages_and_registries/container_registry/explorer/index';
const explorer = registryExplorer();
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index e42e89ce021..b41611001ab 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -3,7 +3,7 @@ import { GlModal } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
@@ -83,7 +83,7 @@ export default {
attributes: [{ variant: 'warning' }],
},
cancelAction: {
- text: s__('Cancel'),
+ text: __('Cancel'),
attributes: [],
},
};
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 80bcbefab46..b365e039191 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -14,7 +14,7 @@ import '~/sourcegraph/load';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const viewBlobEl = document.querySelector('#js-view-blob-app');
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 335d8d481fc..f4beefea90c 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,5 +1,5 @@
import { PROJECT_BADGE } from '~/badges/constants';
-import initConfirmDangerModal from '~/confirm_danger_modal';
+import initLegacyConfirmDangerModal from '~/confirm_danger_modal';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
@@ -9,11 +9,12 @@ import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import setupTransferEdit from '~/transfer_edit';
import UserCallout from '~/user_callout';
+import initTopicsTokenSelector from '~/projects/settings/topics';
import initProjectPermissionsSettings from '../shared/permissions';
import initProjectLoadingSpinner from '../shared/save_project_loader';
initFilePickers();
-initConfirmDangerModal();
+initLegacyConfirmDangerModal();
initSettingsPanels();
initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE);
@@ -28,3 +29,4 @@ setupTransferEdit('.js-project-transfer-form', 'select.select2');
dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr-settings-form'));
initSearchSettings();
+initTopicsTokenSelector();
diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js
index 554ed4f9786..f0554d64ddc 100644
--- a/app/assets/javascripts/pages/projects/environments/index/index.js
+++ b/app/assets/javascripts/pages/projects/environments/index/index.js
@@ -1,3 +1,11 @@
import initEnvironments from '~/environments/';
+import initNewEnvironments from '~/environments/new_index';
-initEnvironments();
+let el = document.getElementById('environments-list-view');
+
+if (el) {
+ initEnvironments(el);
+} else {
+ el = document.getElementById('environments-table');
+ initNewEnvironments(el);
+}
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 795ae713c08..25b62e6c971 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -382,7 +382,11 @@ export default {
:data-testid="`radio-${value}`"
>
<div>
- <gl-icon :name="icon" />
+ <gl-icon
+ data-qa-selector="fork_privacy_button"
+ :name="icon"
+ :data-qa-privacy-level="`${value}`"
+ />
<span>{{ text }}</span>
</div>
<template #help>{{ help }}</template>
diff --git a/app/assets/javascripts/pages/projects/google_cloud/index.js b/app/assets/javascripts/pages/projects/google_cloud/index.js
new file mode 100644
index 00000000000..4506ea8efd1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/index.js
@@ -0,0 +1,3 @@
+import initGoogleCloud from '~/google_cloud/index';
+
+initGoogleCloud();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 62aa5df888f..24aa2f0da13 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,7 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
-import initIncidentApp from '~/issue_show/incident';
+import { initIncidentApp, initIncidentHeaderActions } from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
import initNotesApp from '~/notes';
@@ -22,16 +22,18 @@ export default function initShowIssue() {
switch (issueType) {
case IssuableType.Incident:
initIncidentApp(issuableData);
+ initIncidentHeaderActions(store);
break;
case IssuableType.Issue:
initIssuableApp(issuableData, store);
+ initIssueHeaderActions(store);
break;
default:
+ initIssueHeaderActions(store);
break;
}
initIssuableHeaderWarning(store);
- initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
index 51980b2d971..95afcb6bda8 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
@@ -1,5 +1,6 @@
<script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
@@ -22,6 +23,11 @@ export default {
required: true,
type: Object,
},
+ inviteMembersOpen: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
@@ -33,7 +39,15 @@ export default {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
},
},
+ mounted() {
+ if (this.inviteMembersOpen) {
+ this.openInviteMembersModal('celebrate');
+ }
+ },
methods: {
+ openInviteMembersModal(mode) {
+ eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
+ },
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actions).filter(
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 69fb5878f5c..0995947f3e7 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -40,6 +40,7 @@ export default {
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
+ data-track-experiment="change_continuous_onboarding_link_urls"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index 6da0a8fd212..ea9eec2595f 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
@@ -11,15 +12,17 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
+ const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
- props: { actions, sections },
+ props: { actions, sections, inviteMembersOpen },
});
},
});
}
+initInviteMembersModal();
initLearnGitlab();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index dadf0988582..99094617b0a 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -28,7 +28,6 @@ export default function initMergeRequestShow() {
const el = document.querySelector('.js-mr-status-box');
const apolloProvider = new VueApollo({
- assumeImmutableResults: true,
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
index 95522573b53..f9eecff4ac4 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
@@ -1,10 +1,3 @@
-(async function packageApp() {
- if (window.gon.features.packageListApollo) {
- const newPackageList = await import('~/packages_and_registries/package_registry/pages/list');
+import packageList from '~/packages_and_registries/package_registry/pages/list';
- newPackageList.default();
- } else {
- const packageList = await import('~/packages/list/packages_list_app_bundle');
- packageList.default();
- }
-})();
+packageList();
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 0e646e8c505..85443843684 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
@@ -72,18 +72,18 @@ export default {
return [
{
value: KEY_EVERY_DAY,
- text: sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }),
+ text: sprintf(__(`Every day (at %{time})`), { time: this.formattedTime }),
},
{
value: KEY_EVERY_WEEK,
- text: sprintf(s__('Every week (%{weekday} at %{time})'), {
+ text: sprintf(__('Every week (%{weekday} at %{time})'), {
weekday: this.weekday,
time: this.formattedTime,
}),
},
{
value: KEY_EVERY_MONTH,
- text: sprintf(s__('Every month (Day %{day} at %{time})'), {
+ text: sprintf(__('Every month (Day %{day} at %{time})'), {
day: this.randomDay,
time: this.formattedTime,
}),
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 9e93f709937..a26aeeb6db4 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -23,14 +23,14 @@ export default class Project {
});
}
- $('.hide-no-ssh-message').on('click', function (e) {
+ $('.js-hide-no-ssh-message').on('click', function (e) {
Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
+ $(this).parents('.js-no-ssh-key-message').remove();
return e.preventDefault();
});
- $('.hide-no-password-message').on('click', function (e) {
+ $('.js-hide-no-password-message').on('click', function (e) {
Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
+ $(this).parents('.js-no-password-message').remove();
return e.preventDefault();
});
$('.hide-auto-devops-implicitly-enabled-banner').on('click', function (e) {
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
index 6fd32321568..44579ee1217 100644
--- a/app/assets/javascripts/pages/projects/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -1,4 +1,4 @@
-import registryExplorer from '~/registry/explorer/index';
+import registryExplorer from '~/packages_and_registries/container_registry/explorer/index';
const explorer = registryExplorer();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index b7546a6bed7..cc92a8cd476 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -1,10 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
-
export default {
- components: {
- GlIcon,
- },
props: {
label: {
type: String,
@@ -29,10 +24,14 @@ export default {
<div class="project-feature-row">
<label v-if="label" class="label-bold">
{{ label }}
- <a v-if="helpPath" :href="helpPath" target="_blank">
- <gl-icon name="question-o" />
- </a>
</label>
- <span v-if="helpText" class="form-text text-muted"> {{ helpText }} </span> <slot></slot>
+ <div>
+ <span v-if="helpText" class="text-muted"> {{ helpText }} </span>
+ <span v-if="helpPath"
+ ><a :href="helpPath" target="_blank">{{ __('Learn more') }}</a
+ >.</span
+ >
+ </div>
+ <slot></slot>
</div>
</template>
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 c53d367ed71..384ee1f5034 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
@@ -2,7 +2,7 @@
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import {
visibilityOptions,
visibilityLevelDescriptions,
@@ -31,7 +31,7 @@ export default {
operationsLabel: s__('ProjectSettings|Operations'),
packagesLabel: s__('ProjectSettings|Packages'),
pagesLabel: s__('ProjectSettings|Pages'),
- ciCdLabel: s__('CI/CD'),
+ ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
requirementsLabel: s__('ProjectSettings|Requirements'),
securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
@@ -400,6 +400,9 @@ export default {
ref="project-visibility-settings"
:help-path="visibilityHelpPath"
:label="s__('ProjectSettings|Project visibility')"
+ :help-text="
+ s__('ProjectSettings|Manage who can see the project in the public access directory.')
+ "
>
<div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
<div class="select-wrapper gl-flex-grow-1">
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 78b3f2f1b30..31d69a731fe 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -7,7 +7,7 @@ import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
-import { initUploadFileTrigger } from '~/projects/upload_file_experiment';
+import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import UserCallout from '~/user_callout';
import Star from '../../../star';
diff --git a/app/assets/javascripts/pages/projects/work_items/index/index.js b/app/assets/javascripts/pages/projects/work_items/index.js
index 11c257611f0..11c257611f0 100644
--- a/app/assets/javascripts/pages/projects/work_items/index/index.js
+++ b/app/assets/javascripts/pages/projects/work_items/index.js
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
index 1cb7259be64..e83c73edfde 100644
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -9,12 +9,7 @@ export function initInstallRunner(componentId = 'js-install-runner') {
const installRunnerEl = document.getElementById(componentId);
if (installRunnerEl) {
- const defaultClient = createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- );
+ const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index a8ec731e105..6f19a9f4379 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -12,13 +12,15 @@ import {
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import {
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ WIKI_FORMAT_LABEL,
+ WIKI_FORMAT_UPDATED_ACTION,
} from '../constants';
const trackingMixin = Tracking.mixin({
@@ -83,7 +85,7 @@ export default {
),
},
},
- feedbackTip: s__(
+ feedbackTip: __(
'Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}.',
),
},
@@ -219,6 +221,8 @@ export default {
this.trackFormSubmit();
}
+ this.trackWikiFormat();
+
// Wait until form field values are refreshed
await this.$nextTick();
@@ -304,6 +308,14 @@ export default {
}
},
+ trackWikiFormat() {
+ this.track(WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ value: this.format,
+ extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format },
+ });
+ },
+
dismissContentEditorAlert() {
this.isContentEditorAlertDismissed = true;
},
diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js
index b358ac9cf52..94d086158f1 100644
--- a/app/assets/javascripts/pages/shared/wikis/constants.js
+++ b/app/assets/javascripts/pages/shared/wikis/constants.js
@@ -1,4 +1,5 @@
-export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
-
export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded';
export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor';
+export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
+export const WIKI_FORMAT_LABEL = 'wiki_format';
+export const WIKI_FORMAT_UPDATED_ACTION = 'wiki_format_updated';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 0fab4678bc3..7f4e79976bc 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -5,6 +5,7 @@ import { last } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import { n__, s__, __ } from '~/locale';
const d3 = { select };
@@ -294,7 +295,15 @@ export default class ActivityCalendar {
},
responseType: 'text',
})
- .then(({ data }) => $(this.activitiesContainer).html(data))
+ .then(({ data }) => {
+ $(this.activitiesContainer).html(data);
+ document
+ .querySelector(this.activitiesContainer)
+ .querySelectorAll('.js-localtime')
+ .forEach((el) => {
+ el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
+ });
+ })
.catch(() =>
createFlash({
message: __('An error occurred while retrieving calendar activity'),
diff --git a/app/assets/javascripts/pages/users/terms/index/index.js b/app/assets/javascripts/pages/users/terms/index/index.js
new file mode 100644
index 00000000000..29ddde6da94
--- /dev/null
+++ b/app/assets/javascripts/pages/users/terms/index/index.js
@@ -0,0 +1,4 @@
+import { initTermsApp } from '~/terms';
+import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
+
+waitForCSSLoaded(initTermsApp);
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index f163a7c3a8e..1bb82e1d8e6 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
import RequestWarning from './request_warning.vue';
@@ -55,7 +55,7 @@ export default {
const summary = {};
if (!this.metricDetails.summaryOptions?.hideTotal) {
- summary[s__('Total')] = this.metricDetails.calls;
+ summary[__('Total')] = this.metricDetails.calls;
}
if (!this.metricDetails.summaryOptions?.hideDuration) {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index f1fe8cf10fd..905a5f2d271 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ scrollToCommitForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -52,6 +57,13 @@ export default {
return !(this.message && this.targetBranch);
},
},
+ watch: {
+ scrollToCommitForm(flag) {
+ if (flag) {
+ this.scrollIntoView();
+ }
+ },
+ },
methods: {
onSubmit() {
this.$emit('submit', {
@@ -63,6 +75,10 @@ export default {
onReset() {
this.$emit('cancel');
},
+ scrollIntoView() {
+ this.$el.scrollIntoView({ behavior: 'smooth' });
+ this.$emit('scrolled-to-commit-form');
+ },
},
i18n: {
commitMessage: __('Commit message'),
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 0308cd9c565..14c11099756 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -10,9 +10,8 @@ import {
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
+import updatePipelineEtag from '../../graphql/mutations/update_pipeline_etag.mutation.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
-import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
-import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
import CommitForm from './commit_form.vue';
@@ -41,18 +40,24 @@ export default {
required: false,
default: '',
},
+ isNewCiConfigFile: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ scrollToCommitForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
commit: {},
- isNewCiConfigFile: false,
isSaving: false,
};
},
apollo: {
- isNewCiConfigFile: {
- query: getIsNewCiConfigFile,
- },
currentBranch: {
query: getCurrentBranch,
},
@@ -96,10 +101,10 @@ export default {
content: this.ciFileContent,
lastCommitId: this.commitSha,
},
- update(store, { data }) {
+ update(_, { data }) {
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (pipelineEtag) {
- store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
+ this.$apollo.mutate({ mutation: updatePipelineEtag, variables: pipelineEtag });
}
},
});
@@ -146,6 +151,8 @@ export default {
:current-branch="currentBranch"
:default-message="defaultCommitMessage"
:is-saving="isSaving"
+ :scroll-to-commit-form="scrollToCommitForm"
+ v-on="$listeners"
@cancel="onCommitCancel"
@submit="onCommitSubmit"
/>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index ff1e0b6388f..d7594fb318a 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -2,6 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { experiment } from '~/experimentation/utils';
import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue';
@@ -53,12 +54,23 @@ export default {
},
methods: {
setInitialExpandState() {
+ let isExpanded;
+
+ experiment('pipeline_editor_walkthrough', {
+ control: () => {
+ isExpanded = true;
+ },
+ candidate: () => {
+ isExpanded = false;
+ },
+ });
+
// We check in the local storage and if no value is defined, we want the default
// to be true. We want to explicitly set it to true here so that the drawer
// animates to open on load.
const localValue = localStorage.getItem(this.$options.localDrawerKey);
if (localValue === null) {
- this.isExpanded = true;
+ this.isExpanded = isExpanded;
}
},
setTopPosition() {
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index 68065cc3c73..baf1d17b233 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -12,7 +12,7 @@ import { produce } from 'immer';
import { fetchPolicies } from '~/lib/graphql';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import {
BRANCH_PAGINATION_LIMIT,
BRANCH_SEARCH_DEBOUNCE,
@@ -25,9 +25,9 @@ import getLastCommitBranchQuery from '~/pipeline_editor/graphql/queries/client/l
export default {
i18n: {
- dropdownHeader: s__('Switch branch'),
- title: s__('Branches'),
- fetchError: s__('Unable to fetch branch list for this project.'),
+ dropdownHeader: __('Switch branch'),
+ title: __('Branches'),
+ fetchError: __('Unable to fetch branch list for this project.'),
},
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
@@ -43,14 +43,25 @@ export default {
},
inject: ['projectFullPath', 'totalBranches'],
props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
paginationLimit: {
type: Number,
required: false,
default: BRANCH_PAGINATION_LIMIT,
},
+ shouldLoadNewBranch: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
+ branchSelected: null,
availableBranches: [],
filteredBranches: [],
isSearchingBranches: false,
@@ -101,10 +112,17 @@ export default {
isBranchesLoading() {
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
},
- showBranchSwitcher() {
+ enableBranchSwitcher() {
return this.branches.length > 0 || this.searchTerm.length > 0;
},
},
+ watch: {
+ shouldLoadNewBranch(flag) {
+ if (flag) {
+ this.changeBranch(this.branchSelected);
+ }
+ },
+ },
methods: {
availableBranchesQueryVars(varsOverride = {}) {
if (this.searchTerm.length > 0) {
@@ -149,11 +167,7 @@ export default {
})
.catch(this.showFetchError);
},
- async selectBranch(newBranch) {
- if (newBranch === this.currentBranch) {
- return;
- }
-
+ async changeBranch(newBranch) {
this.updateCurrentBranch(newBranch);
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
@@ -164,6 +178,19 @@ export default {
await this.$nextTick();
this.$emit('refetchContent');
},
+ selectBranch(newBranch) {
+ if (newBranch !== this.currentBranch) {
+ // If there are unsaved changes, we want to show the user
+ // a modal to confirm what to do with these before changing
+ // branches.
+ if (this.hasUnsavedChanges) {
+ this.branchSelected = newBranch;
+ this.$emit('select-branch', newBranch);
+ } else {
+ this.changeBranch(newBranch);
+ }
+ }
+ },
async setSearchTerm(newSearchTerm) {
this.pageCounter = 0;
this.searchTerm = newSearchTerm.trim();
@@ -203,11 +230,11 @@ export default {
<template>
<gl-dropdown
- v-if="showBranchSwitcher"
v-gl-tooltip.hover
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
+ :disabled="!enableBranchSwitcher"
icon="branch"
data-qa-selector="branch_selector_button"
data-testid="branch-selector"
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 551a0430fbf..83b074dd55c 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -5,10 +5,26 @@ export default {
components: {
BranchSwitcher,
},
+ props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldLoadNewBranch: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
};
</script>
<template>
<div class="gl-mb-4">
- <branch-switcher v-on="$listeners" />
+ <branch-switcher
+ :has-unsaved-changes="hasUnsavedChanges"
+ :should-load-new-branch="shouldLoadNewBranch"
+ v-on="$listeners"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
index fcc31f087ff..ec6ee52b6b2 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -63,6 +63,7 @@ export default {
v-if="showPipelineStatus"
:commit-sha="commitSha"
:class="$options.pipelineStatusClasses"
+ v-on="$listeners"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index 75b1398a3c2..25a78aab933 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -1,17 +1,52 @@
<script>
+import { __ } from '~/locale';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import { PIPELINE_FAILURE } from '../../constants';
export default {
+ i18n: {
+ linkedPipelinesFetchError: __('Unable to fetch upstream and downstream pipelines.'),
+ },
components: {
PipelineMiniGraph,
+ LinkedPipelinesMiniList: () =>
+ import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
+ inject: ['projectFullPath'],
props: {
pipeline: {
type: Object,
required: true,
},
},
+ apollo: {
+ linkedPipelines: {
+ query: getLinkedPipelinesQuery,
+ variables() {
+ return {
+ fullPath: this.projectFullPath,
+ iid: this.pipeline.iid,
+ };
+ },
+ skip() {
+ return !this.pipeline.iid;
+ },
+ update({ project }) {
+ return project?.pipeline;
+ },
+ error() {
+ this.$emit('showError', {
+ type: PIPELINE_FAILURE,
+ reasons: [this.$options.i18n.linkedPipelinesFetchError],
+ });
+ },
+ },
+ },
computed: {
+ downstreamPipelines() {
+ return this.linkedPipelines?.downstream?.nodes || [];
+ },
pipelinePath() {
return this.pipeline.detailedStatus?.detailsPath || '';
},
@@ -38,12 +73,29 @@ export default {
};
});
},
+ showDownstreamPipelines() {
+ return this.downstreamPipelines.length > 0;
+ },
+ upstreamPipeline() {
+ return this.linkedPipelines?.upstream;
+ },
},
};
</script>
<template>
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
+ <linked-pipelines-mini-list
+ v-if="upstreamPipeline"
+ :triggered-by="[upstreamPipeline]"
+ data-testid="pipeline-editor-mini-graph-upstream"
+ />
<pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
+ <linked-pipelines-mini-list
+ v-if="showDownstreamPipelines"
+ :triggered="downstreamPipelines"
+ :pipeline-path="pipelinePath"
+ data-testid="pipeline-editor-mini-graph-downstream"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index a1fa2147994..6fe1459c80c 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
@@ -10,7 +10,6 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@@ -21,6 +20,10 @@ export const i18n = {
`Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}`,
),
viewBtn: s__('Pipeline|View pipeline'),
+ viewCommit: s__('Pipeline|View commit'),
+ pipelineNotTriggeredMsg: s__(
+ 'Pipeline|No pipeline was triggered for the latest changes due to the current CI/CD configuration.',
+ ),
};
export default {
@@ -34,7 +37,9 @@ export default {
GlSprintf,
PipelineEditorMiniGraph,
},
- mixins: [glFeatureFlagMixin()],
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -59,12 +64,13 @@ export default {
};
},
update(data) {
- const { id, commitPath = '', detailedStatus = {}, stages, status } =
+ const { id, iid, commit = {}, detailedStatus = {}, stages, status } =
data.project?.pipeline || {};
return {
id,
- commitPath,
+ iid,
+ commit,
detailedStatus,
stages,
status,
@@ -73,20 +79,36 @@ export default {
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
+ } else {
+ this.hasError = true;
+ this.pipelineNotTriggered = true;
}
},
error() {
this.hasError = true;
+ this.networkError = true;
},
pollInterval: POLL_INTERVAL,
},
},
data() {
return {
+ networkError: false,
+ pipelineNotTriggered: false,
hasError: false,
};
},
computed: {
+ commitText() {
+ const shortSha = truncateSha(this.commitSha);
+ const commitTitle = this.pipeline.commit.title || '';
+
+ if (commitTitle.length > 0) {
+ return `${shortSha}: ${commitTitle}`;
+ }
+
+ return shortSha;
+ },
hasPipelineData() {
return Boolean(this.pipeline?.id);
},
@@ -126,13 +148,19 @@ export default {
</div>
</template>
<template v-else-if="hasError">
- <div>
+ <div v-if="networkError">
<gl-icon class="gl-mr-auto" name="warning-solid" />
<span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
</div>
+ <div v-else>
+ <gl-icon class="gl-mr-auto" name="information-o" />
+ <span data-testid="pipeline-not-triggered-error-msg">
+ {{ $options.i18n.pipelineNotTriggeredMsg }}
+ </span>
+ </div>
</template>
<template v-else>
- <div>
+ <div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
<a :href="status.detailsPath" class="gl-mr-auto">
<ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
</a>
@@ -144,25 +172,21 @@ export default {
<template #status>{{ status.text }}</template>
<template #commit>
<gl-link
- :href="pipeline.commitPath"
- class="commit-sha gl-font-weight-normal"
- target="_blank"
+ v-gl-tooltip.hover
+ :href="pipeline.commit.webPath"
+ :title="$options.i18n.viewCommit"
data-testid="pipeline-commit"
>
- {{ shortSha }}
+ {{ commitText }}
</gl-link>
</template>
</gl-sprintf>
</span>
</div>
<div class="gl-display-flex gl-flex-wrap">
- <pipeline-editor-mini-graph
- v-if="glFeatures.pipelineEditorMiniGraph"
- :pipeline="pipeline"
- />
+ <pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
<gl-button
class="gl-mt-2 gl-md-mt-0"
- target="_blank"
category="secondary"
variant="confirm"
:href="status.detailsPath"
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 8bffd893473..611b78b3c5e 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -75,7 +75,7 @@ export default {
return this.$options.i18n.valid;
default:
// Only display first error as a reason
- return this.ciConfig?.errors.length > 0
+ return this.ciConfig?.errors?.length > 0
? sprintf(this.$options.i18n.invalidWithReason, { reason }, false)
: this.$options.i18n.invalid;
}
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index f7c9f10ea46..0cd0d17d944 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -3,15 +3,18 @@ import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
LINT_TAB,
MERGED_TAB,
+ TAB_QUERY_PARAM,
+ TABS_INDEX,
VISUALIZE_TAB,
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
@@ -20,6 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
+import WalkthroughPopover from './walkthrough_popover.vue';
export default {
i18n: {
@@ -42,6 +46,9 @@ export default {
errorTexts: {
loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
},
+ query: {
+ TAB_QUERY_PARAM,
+ },
tabConstants: {
CREATE_TAB,
LINT_TAB,
@@ -58,6 +65,8 @@ export default {
GlTabs,
PipelineGraph,
TextEditor,
+ GitlabExperiment,
+ WalkthroughPopover,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -74,6 +83,10 @@ export default {
required: false,
default: '',
},
+ isNewCiConfigFile: {
+ type: Boolean,
+ required: true,
+ },
},
apollo: {
appStatus: {
@@ -81,9 +94,8 @@ export default {
},
},
computed: {
- hasAppError() {
- // Not an invalid config and with `mergedYaml` data missing
- return this.appStatus === EDITOR_APP_STATUS_ERROR;
+ isMergedYamlAvailable() {
+ return this.ciConfigData?.mergedYaml;
},
isEmpty() {
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
@@ -98,22 +110,51 @@ export default {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
},
+ created() {
+ const [tabQueryParam] = getParameterValues(TAB_QUERY_PARAM);
+
+ if (tabQueryParam && TABS_INDEX[tabQueryParam]) {
+ this.setDefaultTab(tabQueryParam);
+ }
+ },
methods: {
setCurrentTab(tabName) {
this.$emit('set-current-tab', tabName);
},
+ setDefaultTab(tabName) {
+ // We associate tab name with the index so that we can use tab name
+ // in other part of the app and load the corresponding tab closer to the
+ // actual component using a hash that binds the name to the indexes.
+ // This also means that if we ever changed tab order, we would justs need to
+ // update `TABS_INDEX` hash instead of all the instances in the app
+ // where we used the individual indexes
+ const newUrl = setUrlParams({ [TAB_QUERY_PARAM]: TABS_INDEX[tabName] });
+
+ this.setCurrentTab(tabName);
+ updateHistory({ url: newUrl, title: document.title, replace: true });
+ },
},
};
</script>
<template>
- <gl-tabs class="file-editor gl-mb-3">
+ <gl-tabs
+ class="file-editor gl-mb-3"
+ :query-param-name="$options.query.TAB_QUERY_PARAM"
+ sync-active-tab-with-query-params
+ >
<editor-tab
class="gl-mb-3"
+ title-link-class="js-walkthrough-popover-target"
:title="$options.i18n.tabEdit"
lazy
data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
+ <gitlab-experiment name="pipeline_editor_walkthrough">
+ <template #candidate>
+ <walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
+ </template>
+ </gitlab-experiment>
<ci-editor-header />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
@@ -154,7 +195,7 @@ export default {
@click="setCurrentTab($options.tabConstants.MERGED_TAB)"
>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
- <gl-alert v-else-if="hasAppError" variant="danger" :dismissible="false">
+ <gl-alert v-else-if="!isMergedYamlAvailable" variant="danger" :dismissible="false">
{{ $options.errorTexts.loadMergedYaml }}
</gl-alert>
<ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
index 091b202e10b..7206f19d060 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
@@ -8,6 +8,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
+ PIPELINE_FAILURE,
} from '../../constants';
import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
import {
@@ -24,6 +25,7 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ [PIPELINE_FAILURE]: s__('Pipelines|There was a problem with loading the pipeline data.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
@@ -74,6 +76,11 @@ export default {
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
+ case PIPELINE_FAILURE:
+ return {
+ text: this.$options.errorTexts[PIPELINE_FAILURE],
+ variant: 'danger',
+ };
default:
return {
text: this.$options.errorTexts[DEFAULT_FAILURE],
diff --git a/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue b/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue
new file mode 100644
index 00000000000..5742b11b841
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/walkthrough_popover.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButton, GlPopover, GlSprintf, GlOutsideDirective as Outside } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ directives: { Outside },
+ i18n: {
+ title: s__('pipelineEditorWalkthrough|See how GitLab pipelines work'),
+ description: s__(
+ 'pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline.',
+ ),
+ instruction: s__(
+ 'pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline.',
+ ),
+ ctaText: s__("pipelineEditorWalkthrough|Let's do this!"),
+ },
+ components: {
+ GlButton,
+ GlPopover,
+ GlSprintf,
+ },
+ data() {
+ return {
+ show: true,
+ };
+ },
+ computed: {
+ targetElement() {
+ return document.querySelector('.js-walkthrough-popover-target');
+ },
+ },
+ methods: {
+ close() {
+ this.show = false;
+ },
+ handleClickCta() {
+ this.close();
+ this.$emit('walkthrough-popover-cta-clicked');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover
+ :show.sync="show"
+ :title="$options.i18n.title"
+ :target="targetElement"
+ placement="right"
+ triggers="focus"
+ >
+ <div v-outside="close" class="gl-display-flex gl-flex-direction-column">
+ <p>
+ <gl-sprintf :message="$options.i18n.description">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <p>
+ <gl-sprintf :message="$options.i18n.instruction">
+ <template #bold="{ content }">
+ <strong>
+ {{ content }}
+ </strong>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <gl-button
+ class="gl-align-self-end"
+ category="tertiary"
+ data-testid="ctaBtn"
+ variant="confirm"
+ @click="handleClickCta"
+ >
+ <gl-emoji data-name="rocket" />
+ {{ this.$options.i18n.ctaText }}
+ </gl-button>
+ </div>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index bb03fa126a5..a2eaeeef286 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -5,24 +5,37 @@ export const CI_CONFIG_STATUS_VALID = 'VALID';
// Values for EDITOR_APP_STATUS_* are frontend specifics and
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
-export const EDITOR_APP_STATUS_ERROR = 'ERROR';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
+export const EDITOR_APP_VALID_STATUSES = [
+ EDITOR_APP_STATUS_EMPTY,
+ EDITOR_APP_STATUS_INVALID,
+ EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_VALID,
+];
+
export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+export const PIPELINE_FAILURE = 'PIPELINE_FAILURE';
export const CREATE_TAB = 'CREATE_TAB';
export const LINT_TAB = 'LINT_TAB';
export const MERGED_TAB = 'MERGED_TAB';
export const VISUALIZE_TAB = 'VISUALIZE_TAB';
-export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
+export const TABS_INDEX = {
+ [CREATE_TAB]: '0',
+ [VISUALIZE_TAB]: '1',
+ [LINT_TAB]: '2',
+ [MERGED_TAB]: '3',
+};
+export const TAB_QUERY_PARAM = 'tab';
export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE';
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql
new file mode 100644
index 00000000000..7487e328668
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_app_status.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateAppStatus($appStatus: String) {
+ updateAppStatus(appStatus: $appStatus) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql
new file mode 100644
index 00000000000..9025f00b343
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_pipeline_etag.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updatePipelineEtag($pipelineEtag: String) {
+ updatePipelineEtag(pipelineEtag: $pipelineEtag) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql
deleted file mode 100644
index 8c2ca276f50..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/is_new_ci_config_file.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getIsNewCiConfigFile {
- isNewCiConfigFile @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
index 0c3653a2880..34e98ae3eb3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
@@ -1,10 +1,13 @@
query getPipeline($fullPath: ID!, $sha: String!) {
project(fullPath: $fullPath) {
pipeline(sha: $sha) {
- commitPath
id
iid
status
+ commit {
+ title
+ webPath
+ }
detailedStatus {
detailsPath
icon
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index a34652b1495..e4965e00af3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,7 +1,8 @@
-import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
+import getAppStatus from './queries/client/app_status.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
+import getPipelineEtag from './queries/client/pipeline_etag.graphql';
export const resolvers = {
Mutation: {
@@ -31,20 +32,28 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
+ updateAppStatus: (_, { appStatus }, { cache }) => {
+ cache.writeQuery({
+ query: getAppStatus,
+ data: { appStatus },
+ });
+ },
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
- data: produce(cache.readQuery({ query: getCurrentBranchQuery }), (draftData) => {
- draftData.currentBranch = currentBranch;
- }),
+ data: { currentBranch },
});
},
updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
cache.writeQuery({
query: getLastCommitBranchQuery,
- data: produce(cache.readQuery({ query: getLastCommitBranchQuery }), (draftData) => {
- draftData.lastCommitBranch = lastCommitBranch;
- }),
+ data: { lastCommitBranch },
+ });
+ },
+ updatePipelineEtag: (_, { pipelineEtag }, { cache }) => {
+ cache.writeQuery({
+ query: getPipelineEtag,
+ data: { pipelineEtag },
});
},
},
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 89b9091e6f9..4f7f2743aca 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -3,8 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
+import getAppStatus from './graphql/queries/client/app_status.graphql';
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
import { resolvers } from './graphql/resolvers';
@@ -59,12 +61,18 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, {
typeDefs,
useGet: true,
- assumeImmutableResults: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ appStatus: EDITOR_APP_STATUS_LOADING,
+ },
+ });
+
+ cache.writeQuery({
query: getCurrentBranch,
data: {
currentBranch: initialBranchName || defaultBranch,
@@ -93,6 +101,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciExamplesHelpPagePath,
ciHelpPagePath,
configurationPaths,
+ dataMethod: 'graphql',
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index e70417145ab..68db5d8078f 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -12,16 +12,16 @@ import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue
import {
COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
+ EDITOR_APP_VALID_STATUSES,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
+import updateAppStatus from './graphql/mutations/update_app_status.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
-import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
@@ -44,23 +44,23 @@ export default {
},
data() {
return {
- starterTemplateName: STARTER_TEMPLATE_NAME,
ciConfigData: {},
+ currentCiFileContent: '',
failureType: null,
failureReasons: [],
initialCiFileContent: '',
isFetchingCommitSha: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
- currentCiFileContent: '',
- successType: null,
+ shouldSkipStartScreen: false,
+ showFailure: false,
showStartScreen: false,
showSuccess: false,
- showFailure: false,
starterTemplate: '',
+ starterTemplateName: STARTER_TEMPLATE_NAME,
+ successType: null,
};
},
-
apollo: {
initialCiFileContent: {
fetchPolicy: fetchPolicies.NETWORK_ONLY,
@@ -103,7 +103,11 @@ export default {
}
if (!hasCIFile) {
- this.showStartScreen = true;
+ if (this.shouldSkipStartScreen) {
+ this.setNewEmptyCiConfigFile();
+ } else {
+ this.showStartScreen = true;
+ }
} else if (fileContent.length) {
// If the file content is > 0, then we make sure to reset the
// start screen flag during a refetch
@@ -141,10 +145,10 @@ export default {
return { ...ciConfig, stages };
},
result({ data }) {
- this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
+ this.setAppStatus(data?.ciConfig?.status);
},
- error() {
- this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ error(err) {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN, [String(err)]);
},
watchLoading(isLoading) {
if (isLoading) {
@@ -179,9 +183,6 @@ export default {
currentBranch: {
query: getCurrentBranch,
},
- isNewCiConfigFile: {
- query: getIsNewCiConfigFile,
- },
starterTemplate: {
query: getTemplate,
variables() {
@@ -232,6 +233,7 @@ export default {
},
mounted() {
this.loadTemplateFromURL();
+ this.checkShouldSkipStartScreen();
},
methods: {
hideFailure() {
@@ -245,8 +247,6 @@ export default {
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
- this.setAppStatus(EDITOR_APP_STATUS_ERROR);
-
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailure = true;
this.failureType = type;
@@ -261,12 +261,12 @@ export default {
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
- this.$apollo.getClient().writeQuery({ query: getAppStatus, data: { appStatus } });
+ if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
+ this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
+ }
},
setNewEmptyCiConfigFile() {
- this.$apollo
- .getClient()
- .writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: true } });
+ this.isNewCiConfigFile = true;
this.showStartScreen = false;
},
showErrorAlert({ type, reasons = [] }) {
@@ -283,9 +283,7 @@ export default {
this.reportSuccess(type);
if (this.isNewCiConfigFile) {
- this.$apollo
- .getClient()
- .writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: false } });
+ this.isNewCiConfigFile = false;
}
// Keep track of the latest committed content to know
@@ -300,6 +298,10 @@ export default {
this.setNewEmptyCiConfigFile();
}
},
+ checkShouldSkipStartScreen() {
+ const params = queryToObject(window.location.search);
+ this.shouldSkipStartScreen = Boolean(params?.add_new_config_file);
+ },
},
};
</script>
@@ -325,8 +327,9 @@ export default {
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
- :is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
+ :has-unsaved-changes="hasUnsavedChanges"
+ :is-new-ci-config-file="isNewCiConfigFile"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index ba567023946..8e8f31a4acc 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,14 +1,31 @@
<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
-import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
+import { CREATE_TAB } from './constants';
export default {
+ commitSectionRef: 'commitSectionRef',
+ modal: {
+ switchBranch: {
+ title: __('You have unsaved changes'),
+ body: __('Uncommitted changes will be lost if you change branches. Do you want to continue?'),
+ actionPrimary: {
+ text: __('Switch Branches'),
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: { variant: 'default' },
+ },
+ },
+ },
components: {
CommitSection,
+ GlModal,
PipelineEditorDrawer,
PipelineEditorFileNav,
PipelineEditorHeader,
@@ -28,6 +45,11 @@ export default {
required: false,
default: '',
},
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -36,40 +58,82 @@ export default {
data() {
return {
currentTab: CREATE_TAB,
+ scrollToCommitForm: false,
+ shouldLoadNewBranch: false,
+ showSwitchBranchModal: false,
};
},
computed: {
showCommitForm() {
- return TABS_WITH_COMMIT_FORM.includes(this.currentTab);
+ return this.currentTab === CREATE_TAB;
},
},
methods: {
+ closeBranchModal() {
+ this.showSwitchBranchModal = false;
+ },
+ handleConfirmSwitchBranch() {
+ this.showSwitchBranchModal = true;
+ },
+ switchBranch() {
+ this.showSwitchBranchModal = false;
+ this.shouldLoadNewBranch = true;
+ },
setCurrentTab(tabName) {
this.currentTab = tabName;
},
+ setScrollToCommitForm(newValue = true) {
+ this.scrollToCommitForm = newValue;
+ },
},
};
</script>
<template>
<div class="gl-pr-9 gl-transition-medium gl-w-full">
- <pipeline-editor-file-nav v-on="$listeners" />
+ <gl-modal
+ v-if="showSwitchBranchModal"
+ visible
+ modal-id="switchBranchModal"
+ :title="$options.modal.switchBranch.title"
+ :action-primary="$options.modal.switchBranch.actionPrimary"
+ :action-secondary="$options.modal.switchBranch.actionSecondary"
+ @primary="switchBranch"
+ @secondary="closeBranchModal"
+ @cancel="closeBranchModal"
+ @hide="closeBranchModal"
+ >
+ {{ $options.modal.switchBranch.body }}
+ </gl-modal>
+ <pipeline-editor-file-nav
+ :has-unsaved-changes="hasUnsavedChanges"
+ :should-load-new-branch="shouldLoadNewBranch"
+ @select-branch="handleConfirmSwitchBranch"
+ v-on="$listeners"
+ />
<pipeline-editor-header
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
+ v-on="$listeners"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners"
@set-current-tab="setCurrentTab"
+ @walkthrough-popover-cta-clicked="setScrollToCommitForm"
/>
<commit-section
v-if="showCommitForm"
+ :ref="$options.commitSectionRef"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
+ :is-new-ci-config-file="isNewCiConfigFile"
+ :scroll-to-commit-form="scrollToCommitForm"
+ @scrolled-to-commit-form="setScrollToCommitForm(false)"
v-on="$listeners"
/>
<pipeline-editor-drawer />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index b778fe28e59..9725e882d5e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -95,9 +95,9 @@ export default {
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
- <gl-loading-icon v-if="isLoading" size="sm" />
+ <gl-loading-icon v-else-if="isLoading" size="sm" />
- <gl-dropdown-item v-if="!artifacts.length && !isLoading" data-testid="artifacts-empty-message">
+ <gl-dropdown-item v-else-if="!artifacts.length" data-testid="artifacts-empty-message">
{{ $options.i18n.emptyArtifactsMessage }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 1c7c4d7c704..7d0cea67099 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,20 +1,15 @@
<script>
import {
- GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
export const i18n = {
artifacts: __('Artifacts'),
artifactSectionHeader: __('Download artifacts'),
- artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
- noArtifacts: s__('Pipelines|No artifacts available'),
};
export default {
@@ -23,11 +18,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlAlert,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLoadingIcon,
},
inject: {
artifactsEndpoint: {
@@ -42,44 +35,22 @@ export default {
type: Number,
required: true,
},
- },
- data() {
- return {
- artifacts: [],
- hasError: false,
- isLoading: false,
- };
- },
- computed: {
- hasArtifacts() {
- return Boolean(this.artifacts.length);
+ artifacts: {
+ type: Array,
+ required: false,
+ default: () => [],
},
},
- methods: {
- fetchArtifacts() {
- this.isLoading = true;
- // Replace the placeholder with the ID of the pipeline we are viewing
- const endpoint = this.artifactsEndpoint.replace(
- this.artifactsEndpointPlaceholder,
- this.pipelineId,
- );
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.artifacts = data.artifacts;
- })
- .catch(() => {
- this.hasError = true;
- })
- .finally(() => {
- this.isLoading = false;
- });
+ computed: {
+ shouldShowDropdown() {
+ return this.artifacts?.length;
},
},
};
</script>
<template>
<gl-dropdown
+ v-if="shouldShowDropdown"
v-gl-tooltip
class="build-artifacts js-pipeline-dropdown-download"
:title="$options.i18n.artifacts"
@@ -89,22 +60,11 @@ export default {
right
lazy
text-sr-only
- @show.once="fetchArtifacts"
>
<gl-dropdown-section-header>{{
$options.i18n.artifactSectionHeader
}}</gl-dropdown-section-header>
- <gl-alert v-if="hasError" variant="danger" :dismissible="false">
- {{ $options.i18n.artifactsFetchErrorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-if="isLoading" size="sm" />
-
- <gl-alert v-else-if="!hasArtifacts" variant="info" :dismissible="false">
- {{ $options.i18n.noArtifacts }}
- </gl-alert>
-
<gl-dropdown-item
v-for="(artifact, i) in artifacts"
:key="i"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
index c6c81d5253b..83f6356f31a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
@@ -76,7 +76,7 @@ export default {
</p>
<div class="row gl-mb-8">
- <div class="col-lg-3">
+ <div class="col-12">
<gl-card>
<div class="gl-flex-direction-row">
<div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 12ee82f0390..d64decc81ec 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTable, GlTooltipDirective } from '@gitlab/ui';
+import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue';
@@ -18,7 +18,7 @@ const DEFAULT_TH_CLASSES =
export default {
components: {
- GlTable,
+ GlTableLite,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
@@ -156,7 +156,7 @@ export default {
</script>
<template>
<div class="ci-table">
- <gl-table
+ <gl-table-lite
:fields="tableFields"
:items="pipelines"
tbody-tr-class="commit"
@@ -225,7 +225,7 @@ export default {
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template>
- </gl-table>
+ </gl-table-lite>
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 5678b613ec6..d123f7a203c 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -49,3 +49,5 @@ export const PipelineKeyOptions = [
key: 'iid',
},
];
+
+export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
index de8de651eea..8fcae9dbad8 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
@@ -18,8 +18,11 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
}
createdAt
user {
+ id
name
+ username
webPath
+ webUrl
email
avatarUrl
status {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 082d67c938c..3201f88a9e3 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -4,7 +4,7 @@ import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/commo
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import { validateParams } from '~/pipelines/utils';
-import { CANCEL_REQUEST } from '../constants';
+import { CANCEL_REQUEST, TOAST_MESSAGE } from '../constants';
import eventHub from '../event_hub';
export default {
@@ -191,7 +191,10 @@ export default {
this.service
.runMRPipeline(options)
- .then(() => this.updateTable())
+ .then(() => {
+ this.$toast.show(TOAST_MESSAGE);
+ this.updateTable();
+ })
.catch(() => {
createFlash({
message: __(
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/pipelines/pipeline_shared_client.js
index 7a922acd0b3..c3be487caae 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/pipelines/pipeline_shared_client.js
@@ -5,7 +5,6 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
- assumeImmutableResults: true,
useGet: true,
},
),
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 17cbcabeedb..3cb2dce87d3 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -37,7 +37,7 @@ export default {
},
primaryProps() {
return {
- text: s__('Delete account'),
+ text: __('Delete account'),
attributes: [
{ variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' },
{ category: 'primary' },
@@ -47,7 +47,7 @@ export default {
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
canSubmit() {
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 7917a9a75e0..45a6130826d 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -3,7 +3,7 @@ import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective }
import { escape } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
@@ -58,7 +58,7 @@ Please update your Git repository remotes as soon as possible.`),
},
primaryProps() {
return {
- text: s__('Update username'),
+ text: __('Update username'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
@@ -68,7 +68,7 @@ Please update your Git repository remotes as soon as possible.`),
},
cancelProps() {
return {
- text: s__('Cancel'),
+ text: __('Cancel'),
};
},
},
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
index e3868e2925d..1b57a69d464 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/project_visibility.js
@@ -1,42 +1,58 @@
import $ from 'jquery';
+import eventHub from '~/projects/new/event_hub';
-function setVisibilityOptions(namespaceSelector) {
- if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
- return;
- }
- const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
- const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
+// Values are from lib/gitlab/visibility_level.rb
+const visibilityLevel = {
+ private: 0,
+ internal: 10,
+ public: 20,
+};
+function setVisibilityOptions({ name, visibility, showPath, editPath }) {
document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
+ // Don't change anything if the option is restricted by admin
+ if (option.classList.contains('restricted')) {
+ return;
+ }
+
const optionInput = option.querySelector('input[type=radio]');
- const optionValue = optionInput ? optionInput.value : 0;
- const optionTitle = option.querySelector('.option-title');
- const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
+ const optionValue = optionInput ? parseInt(optionInput.value, 10) : 0;
- // don't change anything if the option is restricted by admin
- if (!option.classList.contains('restricted')) {
- if (visibilityLevel < optionValue) {
- option.classList.add('disabled');
- optionInput.disabled = true;
- const reason = option.querySelector('.option-disabled-reason');
- if (reason) {
- reason.innerHTML = `This project cannot be ${optionName} because the visibility of
+ if (visibilityLevel[visibility] < optionValue) {
+ option.classList.add('disabled');
+ optionInput.disabled = true;
+ const reason = option.querySelector('.option-disabled-reason');
+ if (reason) {
+ const optionTitle = option.querySelector('.option-title');
+ const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
+ reason.innerHTML = `This project cannot be ${optionName} because the visibility of
<a href="${showPath}">${name}</a> is ${visibility}. To make this project
${optionName}, you must first <a href="${editPath}">change the visibility</a>
of the parent group.`;
- }
- } else {
- option.classList.remove('disabled');
- optionInput.disabled = false;
}
+ } else {
+ option.classList.remove('disabled');
+ optionInput.disabled = false;
}
});
}
+function handleSelect2DropdownChange(namespaceSelector) {
+ if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
+ return;
+ }
+ const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
+ setVisibilityOptions(selectedNamespace.dataset);
+}
+
export default function initProjectVisibilitySelector() {
+ eventHub.$on('update-visibility', setVisibilityOptions);
+
const namespaceSelector = document.querySelector('select.js-select-namespace');
if (namespaceSelector) {
- $('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector));
- setVisibilityOptions(namespaceSelector);
+ $('.select2.js-select-namespace').on('change', () =>
+ handleSelect2DropdownChange(namespaceSelector),
+ );
+ handleSelect2DropdownChange(namespaceSelector);
}
}
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index ec7d37644a8..f9dd72119d1 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -1,6 +1,7 @@
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
+import api from '~/api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf';
import eventHub from '../event_hub';
@@ -40,6 +41,11 @@ export default {
required: false,
default: false,
},
+ primaryActionEventName: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -83,6 +89,10 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
handlePrimary() {
+ if (this.primaryActionEventName) {
+ api.trackRedisHllUserEvent(this.primaryActionEventName);
+ }
+
this.$refs.form.$el.submit();
},
resetModalHandler() {
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
index 47ee8237fea..b21fd1a74de 100644
--- a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
@@ -9,7 +9,7 @@ import {
} from './constants';
import createStore from './store';
-export default function initInviteMembersModal() {
+export default function initInviteMembersModal(primaryActionEventName) {
const el = document.querySelector('.js-cherry-pick-commit-modal');
if (!el) {
return false;
@@ -52,6 +52,7 @@ export default function initInviteMembersModal() {
openModal: OPEN_CHERRY_PICK_MODAL,
modalId: CHERRY_PICK_MODAL_ID,
isCherryPick: true,
+ primaryActionEventName,
},
}),
});
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
index df26aa3c830..849b2f4858c 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -10,7 +10,7 @@ import {
} from './constants';
import createStore from './store';
-export default function initInviteMembersModal() {
+export default function initInviteMembersModal(primaryActionEventName) {
const el = document.querySelector('.js-revert-commit-modal');
if (!el) {
return false;
@@ -49,6 +49,7 @@ export default function initInviteMembersModal() {
i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL },
openModal: OPEN_REVERT_MODAL,
modalId: REVERT_MODAL_ID,
+ primaryActionEventName,
},
}),
});
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index 2505c47147f..1d4ec4c110b 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,12 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 06711e4025a..eaf93e2da4f 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -18,12 +18,36 @@ export default {
type: String,
required: true,
},
+ isFork: {
+ type: Boolean,
+ required: true,
+ },
+ issuesCount: {
+ type: Number,
+ required: true,
+ },
+ mergeRequestsCount: {
+ type: Number,
+ required: true,
+ },
+ forksCount: {
+ type: Number,
+ required: true,
+ },
+ starsCount: {
+ type: Number,
+ required: true,
+ },
},
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
+ 'After a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
+ ),
+ isNotForkMessage: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
),
+ isForkMessage: __('This forked project has the following:'),
},
};
</script>
@@ -37,6 +61,38 @@ export default {
:title="$options.strings.alertTitle"
:dismissible="false"
>
+ <p>
+ <gl-sprintf v-if="isFork" :message="$options.strings.isForkMessage" />
+ <gl-sprintf v-else :message="$options.strings.isNotForkMessage">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <ul>
+ <li>
+ <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
+ <template #issuesCount>{{ issuesCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf
+ :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
+ >
+ <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
+ <template #forksCount>{{ forksCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
+ <template #starsCount>{{ starsCount }}</template>
+ </gl-sprintf>
+ </li>
+ </ul>
<gl-sprintf :message="$options.strings.alertBody">
<template #strong="{ content }">
<strong>{{ content }}</strong>
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 2da9449d24e..0393d82ca36 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -93,6 +93,10 @@ export default {
text: s__('ProjectTemplates|Serverless Framework/JS'),
icon: '.template-option .icon-serverless_framework',
},
+ tencent_serverless_framework: {
+ text: s__('ProjectTemplates|Tencent Serverless Framework/NextjsSSR'),
+ icon: '.template-option .icon-tencent_serverless_framework',
+ },
cluster_management: {
text: s__('ProjectTemplates|GitLab Cluster Management'),
icon: '.template-option .icon-cluster_management',
diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
index 5b19f15c233..e1c8c66a214 100644
--- a/app/assets/javascripts/projects/details/upload_button.vue
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-import { trackFileUploadEvent } from '../upload_file_experiment_tracking';
const UPLOAD_BLOB_MODAL_ID = 'details-modal-upload-blob';
@@ -30,11 +29,6 @@ export default {
default: '',
},
},
- methods: {
- trackOpenModal() {
- trackFileUploadEvent('click_upload_modal_trigger');
- },
- },
uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
};
</script>
@@ -44,7 +38,6 @@ export default {
v-gl-modal="$options.uploadBlobModalId"
icon="upload"
data-testid="upload-file-button"
- @click="trackOpenModal"
>{{ __('Upload File') }}</gl-button
>
<upload-blob-modal
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index 6e9efc50be8..476d6466cbb 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -95,7 +95,7 @@ export default {
<template>
<new-namespace-page
- :initial-breadcrumb="s__('New project')"
+ :initial-breadcrumb="__('New project')"
:panels="availablePanels"
:jump-to-last-persisted-panel="hasErrors"
:title="s__('ProjectsNew|Create new project')"
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index bf44ff70562..e0ba60074af 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -6,9 +6,9 @@ import {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
@@ -24,7 +24,6 @@ export default {
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
- GlLoadingIcon,
GlSearchBoxByType,
},
mixins: [Tracking.mixin()],
@@ -103,6 +102,15 @@ export default {
focusInput() {
this.$refs.search.focusInput();
},
+ handleDropdownItemClick(namespace) {
+ eventHub.$emit('update-visibility', {
+ name: namespace.name,
+ visibility: namespace.visibility,
+ showPath: namespace.webUrl,
+ editPath: joinPaths(namespace.webUrl, '-', 'edit'),
+ });
+ this.setNamespace(namespace);
+ },
handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
@@ -134,23 +142,23 @@ export default {
<gl-search-box-by-type
ref="search"
v-model.trim="search"
+ :is-loading="$apollo.queries.currentUser.loading"
data-qa-selector="select_namespace_dropdown_search_field"
/>
- <gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
- <template v-else>
+ <template v-if="!$apollo.queries.currentUser.loading">
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group of filteredGroups"
:key="group.id"
- @click="setNamespace(group)"
+ @click="handleDropdownItemClick(group)"
>
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="setNamespace(userNamespace)">
+ <gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
@@ -158,6 +166,11 @@ export default {
</template>
</gl-dropdown>
- <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
+ <input
+ id="project_namespace_id"
+ type="hidden"
+ name="project[namespace_id]"
+ :value="selectedNamespace.id"
+ />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
index 572d3276e4f..010c6a29ae3 100644
--- a/app/assets/javascripts/projects/new/index.js
+++ b/app/assets/javascripts/projects/new/index.js
@@ -50,7 +50,7 @@ export function initNewProjectUrlSelect() {
new Vue({
el,
apolloProvider: new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
}),
provide: {
namespaceFullPath: el.dataset.namespaceFullPath,
diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
index e16fe5dde49..74febec5a51 100644
--- a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
+++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
@@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) {
nodes {
id
fullPath
+ name
+ visibility
+ webUrl
}
}
namespace {
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 25bacc1cc4a..7379d5caed7 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -11,12 +11,17 @@ export default {
DeploymentFrequencyCharts: () =>
import('ee_component/dora/components/deployment_frequency_charts.vue'),
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
+ ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
inject: {
shouldRenderDoraCharts: {
type: Boolean,
default: false,
},
+ shouldRenderQualitySummary: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
return {
@@ -31,6 +36,10 @@ export default {
chartsToShow.push('deployment-frequency', 'lead-time');
}
+ if (this.shouldRenderQualitySummary) {
+ chartsToShow.push('project-quality');
+ }
+
return chartsToShow;
},
},
@@ -68,6 +77,9 @@ export default {
<lead-time-charts />
</gl-tab>
</template>
+ <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')">
+ <project-quality-summary />
+ </gl-tab>
</gl-tabs>
<pipeline-charts v-else />
</div>
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index f7ea89068a0..003b61d94b1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -7,13 +7,14 @@ import ProjectPipelinesCharts from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const mountPipelineChartsApp = (el) => {
const { projectPath } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
+ const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary);
return new Vue({
el,
@@ -25,6 +26,7 @@ const mountPipelineChartsApp = (el) => {
provide: {
projectPath,
shouldRenderDoraCharts,
+ shouldRenderQualitySummary,
},
render: (createElement) => createElement(ProjectPipelinesCharts, {}),
});
diff --git a/app/assets/javascripts/projects/project_delete_button.js b/app/assets/javascripts/projects/project_delete_button.js
index aa7fc31d307..b4d388eda3a 100644
--- a/app/assets/javascripts/projects/project_delete_button.js
+++ b/app/assets/javascripts/projects/project_delete_button.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import ProjectDeleteButton from './components/project_delete_button.vue';
export default (selector = '#js-project-delete-button') => {
@@ -6,7 +7,15 @@ export default (selector = '#js-project-delete-button') => {
if (!el) return;
- const { confirmPhrase, formPath } = el.dataset;
+ const {
+ confirmPhrase,
+ formPath,
+ isFork,
+ issuesCount,
+ mergeRequestsCount,
+ forksCount,
+ starsCount,
+ } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -16,6 +25,11 @@ export default (selector = '#js-project-delete-button') => {
props: {
confirmPhrase,
formPath,
+ isFork: parseBoolean(isFork),
+ issuesCount: parseInt(issuesCount, 10),
+ mergeRequestsCount: parseInt(mergeRequestsCount, 10),
+ forksCount: parseInt(forksCount, 10),
+ starsCount: parseInt(starsCount, 10),
},
});
},
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index b350db0c838..8d71a3dab68 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -43,6 +43,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr
};
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
+ const specialRepo = document.querySelector('.js-user-readme-repo');
+
// eslint-disable-next-line @gitlab/no-global-event-off
$projectNameInput.off('keyup change').on('keyup change', () => {
onProjectNameChange($projectNameInput, $projectPathInput);
@@ -54,6 +56,11 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
$projectPathInput.off('keyup change').on('keyup change', () => {
onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName);
hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0;
+
+ specialRepo.classList.toggle(
+ 'gl-display-none',
+ $projectPathInput.val() !== $projectPathInput.data('username'),
+ );
});
};
diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
new file mode 100644
index 00000000000..e8b0e95b142
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlTokenSelector, GlAvatarLabeled } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import searchProjectTopics from '../queries/project_topics_search.query.graphql';
+
+export default {
+ components: {
+ GlTokenSelector,
+ GlAvatarLabeled,
+ },
+ i18n: {
+ placeholder: s__('ProjectSettings|Search for topic'),
+ },
+ props: {
+ selected: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ apollo: {
+ topics: {
+ query: searchProjectTopics,
+ variables() {
+ return {
+ search: this.search,
+ };
+ },
+ update(data) {
+ return (
+ data.topics?.nodes.filter(
+ (topic) => !this.selectedTokens.some((token) => token.name === topic.name),
+ ) || []
+ );
+ },
+ debounce: 250,
+ },
+ },
+ data() {
+ return {
+ topics: [],
+ selectedTokens: this.selected,
+ search: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.topics.loading;
+ },
+ placeholderText() {
+ return this.selectedTokens.length ? '' : this.$options.i18n.placeholder;
+ },
+ },
+ methods: {
+ handleEnter(event) {
+ // Prevent form from submitting when adding a token
+ if (event.target.value !== '') {
+ event.preventDefault();
+ }
+ },
+ filterTopics(searchTerm) {
+ this.search = searchTerm;
+ },
+ onTokensUpdate(tokens) {
+ this.$emit('update', tokens);
+ },
+ },
+};
+</script>
+<template>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="selectedTokens"
+ :dropdown-items="topics"
+ :loading="loading"
+ allow-user-defined-tokens
+ :placeholder="placeholderText"
+ @keydown.enter="handleEnter"
+ @text-input="filterTopics"
+ @input="onTokensUpdate"
+ >
+ <template #dropdown-item-content="{ dropdownItem }">
+ <gl-avatar-labeled
+ :src="dropdownItem.avatarUrl"
+ :entity-name="dropdownItem.name"
+ :label="dropdownItem.name"
+ :size="32"
+ shape="rect"
+ />
+ </template>
+ </gl-token-selector>
+</template>
diff --git a/app/assets/javascripts/projects/settings/topics/index.js b/app/assets/javascripts/projects/settings/topics/index.js
new file mode 100644
index 00000000000..3fbd1a61abe
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/index.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import TopicsTokenSelector from './components/topics_token_selector.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.querySelector('.js-topics-selector');
+
+ if (!el) return null;
+
+ const { hiddenInputId } = el.dataset;
+ const hiddenInput = document.getElementById(hiddenInputId);
+
+ const selected = hiddenInput.value
+ ? hiddenInput.value.split(/,\s*/).map((token, index) => ({
+ id: index,
+ name: token,
+ }))
+ : [];
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(createElement) {
+ return createElement(TopicsTokenSelector, {
+ props: {
+ selected,
+ },
+ on: {
+ update(tokens) {
+ const value = tokens.map(({ name }) => name).join(', ');
+ hiddenInput.value = value;
+ // Dispatch `input` event so form submit button becomes active
+ hiddenInput.dispatchEvent(
+ new Event('input', {
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
new file mode 100644
index 00000000000..b193165062a
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql
@@ -0,0 +1,9 @@
+query searchProjectTopics($search: String) {
+ topics(search: $search) {
+ nodes {
+ id
+ name
+ avatarUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 4c083ed5496..14c8c53dd19 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -31,6 +31,9 @@ export default {
selectedTemplate: {
default: '',
},
+ selectedFileTemplateProjectId: {
+ default: null,
+ },
outgoingName: {
default: '',
},
@@ -80,7 +83,7 @@ export default {
});
},
- onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
+ onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
this.isTemplateSaving = true;
const body = {
@@ -88,6 +91,7 @@ export default {
outgoing_name: outgoingName,
project_key: projectKey,
service_desk_enabled: this.isEnabled,
+ file_template_project_id: fileTemplateProjectId,
};
return axios
@@ -132,6 +136,7 @@ export default {
:custom-email="updatedCustomEmail"
:custom-email-enabled="customEmailEnabled"
:initial-selected-template="selectedTemplate"
+ :initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
:templates="templates"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index fe2d376f1da..b8053bf9ab5 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,15 +1,8 @@
<script>
-import {
- GlButton,
- GlFormSelect,
- GlToggle,
- GlLoadingIcon,
- GlSprintf,
- GlFormInput,
- GlLink,
-} from '@gitlab/ui';
+import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
export default {
i18n: {
@@ -18,12 +11,12 @@ export default {
components: {
ClipboardButton,
GlButton,
- GlFormSelect,
GlToggle,
GlLoadingIcon,
GlSprintf,
GlFormInput,
GlLink,
+ ServiceDeskTemplateDropdown,
},
props: {
isEnabled: {
@@ -49,6 +42,11 @@ export default {
required: false,
default: '',
},
+ initialSelectedFileTemplateProjectId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
initialOutgoingName: {
type: String,
required: false,
@@ -73,14 +71,14 @@ export default {
data() {
return {
selectedTemplate: this.initialSelectedTemplate,
+ selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ searchTerm: '',
+ projectKeyError: null,
};
},
computed: {
- templateOptions() {
- return [''].concat(this.templates);
- },
hasProjectKeySupport() {
return Boolean(this.customEmailEnabled);
},
@@ -100,8 +98,21 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
+ templateChange({ selectedFileTemplateProjectId, selectedTemplate }) {
+ this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
+ this.selectedTemplate = selectedTemplate;
+ },
+ validateProjectKey() {
+ if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) {
+ this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.');
+ return;
+ }
+
+ this.projectKeyError = null;
+ },
},
};
</script>
@@ -167,8 +178,17 @@ export default {
v-model.trim="projectKey"
data-testid="project-suffix"
class="form-control"
+ :state="!projectKeyError"
+ @blur="validateProjectKey"
/>
- <span v-if="hasProjectKeySupport" class="form-text text-muted">
+ <span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger">
+ {{ projectKeyError }}
+ </span>
+ <span
+ v-if="hasProjectKeySupport"
+ class="form-text text-muted"
+ :class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }"
+ >
{{ __('A string appended to the project path to form the Service Desk email address.') }}
</span>
<span v-else class="form-text text-muted">
@@ -193,12 +213,13 @@ export default {
<label for="service-desk-template-select" class="mt-3">
{{ __('Template to append to all Service Desk issues') }}
</label>
- <gl-form-select
- id="service-desk-template-select"
- v-model="selectedTemplate"
- data-qa-selector="service_desk_template_dropdown"
- :options="templateOptions"
+ <service-desk-template-dropdown
+ :selected-template="selectedTemplate"
+ :selected-file-template-project-id="selectedFileTemplateProjectId"
+ :templates="templates"
+ @change="templateChange"
/>
+
<label for="service-desk-email-from-name" class="mt-3">
{{ __('Email display name') }}
</label>
@@ -210,6 +231,7 @@ export default {
<gl-button
variant="success"
class="gl-mt-5"
+ data-testid="save_service_desk_settings_button"
data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving"
@click="onSaveTemplate"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
new file mode 100644
index 00000000000..bdd9f940d79
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
@@ -0,0 +1,115 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ selectedTemplate: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ templates: {
+ type: Array,
+ required: true,
+ },
+ selectedFileTemplateProjectId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ };
+ },
+ computed: {
+ templateOptions() {
+ if (this.searchTerm) {
+ const filteredTemplates = [];
+ for (let i = 0; i < this.templates.length; i += 2) {
+ const sectionName = this.templates[i];
+ const availableTemplates = this.templates[i + 1];
+
+ const matchedTemplates = fuzzaldrinPlus.filter(availableTemplates, this.searchTerm, {
+ key: 'name',
+ });
+
+ if (matchedTemplates.length > 0) {
+ filteredTemplates.push(sectionName, matchedTemplates);
+ }
+ }
+
+ return filteredTemplates;
+ }
+
+ return this.templates;
+ },
+ },
+ methods: {
+ templateClick(template) {
+ // Clicking on the same template should unselect it
+ if (
+ template.name === this.selectedTemplate &&
+ template.project_id === this.selectedFileTemplateProjectId
+ ) {
+ this.$emit('change', {
+ selectedFileTemplateProjectId: null,
+ selectedTemplate: null,
+ });
+ return;
+ }
+
+ this.$emit('change', {
+ selectedFileTemplateProjectId: template.project_id,
+ selectedTemplate: template.key,
+ });
+ },
+ },
+ i18n: {
+ defaultDropdownText: __('Choose a template'),
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ id="service-desk-template-select"
+ :text="selectedTemplate || $options.i18n.defaultDropdownText"
+ :header-text="$options.i18n.defaultDropdownText"
+ data-qa-selector="service_desk_template_dropdown"
+ :block="true"
+ class="service-desk-template-select"
+ toggle-class="gl-m-0"
+ >
+ <template #header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <template v-for="item in templateOptions">
+ <gl-dropdown-section-header v-if="!Array.isArray(item)" :key="item">
+ {{ item }}
+ </gl-dropdown-section-header>
+ <template v-else>
+ <gl-dropdown-item
+ v-for="template in item"
+ :key="template.key"
+ :is-check-item="true"
+ :is-checked="
+ template.project_id === selectedFileTemplateProjectId &&
+ template.name === selectedTemplate
+ "
+ @click="() => templateClick(template)"
+ >
+ {{ template.name }}
+ </gl-dropdown-item>
+ </template>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index f842ffaaa2b..e14cdee17ce 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -18,6 +18,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
+ selectedFileTemplateProjectId,
templates,
} = el.dataset;
@@ -32,6 +33,7 @@ export default () => {
outgoingName,
projectKey,
selectedTemplate,
+ selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
},
render: (createElement) => createElement(ServiceDeskRoot),
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
index 7047fd925fb..a42a9711572 100644
--- a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
+++ b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
@@ -1,9 +1,10 @@
<script>
-import { GlLink, GlIcon, GlTable, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlIcon, GlTableLite as GlTable, GlSprintf } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { thWidthClass } from '~/lib/utils/table_utility';
import { sprintf } from '~/locale';
import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
+import StorageTypeIcon from './storage_type_icon.vue';
export default {
name: 'StorageTable',
@@ -12,6 +13,7 @@ export default {
GlIcon,
GlTable,
GlSprintf,
+ StorageTypeIcon,
},
props: {
storageTypes: {
@@ -48,31 +50,39 @@ export default {
<template>
<gl-table :items="storageTypes" :fields="$options.projectTableFields">
<template #cell(storageType)="{ item }">
- <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
- {{ item.storageType.name }}
- <gl-link
- v-if="item.storageType.helpPath"
- :href="item.storageType.helpPath"
- target="_blank"
- :aria-label="helpLinkAriaLabel(item.storageType.name)"
- :data-testid="`${item.storageType.id}-help-link`"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </p>
- <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
- {{ item.storageType.description }}
- </p>
- <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
- <gl-icon name="warning" :size="12" />
- <gl-sprintf :message="item.storageType.warningMessage">
- <template #warningLink="{ content }">
- <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <div class="gl-display-flex gl-flex-direction-row">
+ <storage-type-icon
+ :name="item.storageType.id"
+ :data-testid="`${item.storageType.id}-icon`"
+ />
+ <div>
+ <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
+ {{ item.storageType.name }}
+ <gl-link
+ v-if="item.storageType.helpPath"
+ :href="item.storageType.helpPath"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel(item.storageType.name)"
+ :data-testid="`${item.storageType.id}-help-link`"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </p>
+ <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
+ {{ item.storageType.description }}
+ </p>
+ <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
+ <gl-icon name="warning" :size="12" />
+ <gl-sprintf :message="item.storageType.warningMessage">
+ <template #warningLink="{ content }">
+ <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </div>
</template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
new file mode 100644
index 00000000000..bc7cd42df1e
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlIcon },
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ iconName(storageTypeName) {
+ const defaultStorageTypeIcon = 'disk';
+ const storageTypeIconMap = {
+ lfsObjectsSize: 'doc-image',
+ snippetsSize: 'snippet',
+ uploadsSize: 'upload',
+ repositorySize: 'infrastructure-registry',
+ packagesSize: 'package',
+ };
+
+ return storageTypeIconMap[`${storageTypeName}`] ?? defaultStorageTypeIcon;
+ },
+ },
+};
+</script>
+<template>
+ <span
+ class="gl-display-inline-flex gl-align-items-flex-start gl-justify-content-center gl-min-w-8 gl-pr-2 gl-pt-1"
+ >
+ <gl-icon :name="iconName(name)" :size="16" class="gl-mt-1" />
+ </span>
+</template>
diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js
index d9b28abfbe7..df4b1800dff 100644
--- a/app/assets/javascripts/projects/storage_counter/constants.js
+++ b/app/assets/javascripts/projects/storage_counter/constants.js
@@ -6,13 +6,13 @@ export const PROJECT_STORAGE_TYPES = [
name: s__('UsageQuota|Artifacts'),
description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
warningMessage: s__(
- 'UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ 'UsageQuota|Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.',
),
warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380',
},
{
id: 'lfsObjectsSize',
- name: s__('UsageQuota|LFS Storage'),
+ name: s__('UsageQuota|LFS storage'),
description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
},
{
@@ -23,7 +23,7 @@ export const PROJECT_STORAGE_TYPES = [
{
id: 'repositorySize',
name: s__('UsageQuota|Repository'),
- description: s__('UsageQuota|Git repository, managed by the Gitaly service.'),
+ description: s__('UsageQuota|Git repository.'),
},
{
id: 'snippetsSize',
@@ -51,11 +51,11 @@ export const ERROR_MESSAGE = s__(
'UsageQuota|Something went wrong while fetching project storage statistics',
);
-export const LEARN_MORE_LABEL = s__('Learn more.');
+export const LEARN_MORE_LABEL = __('Learn more.');
export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A');
-export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage Breakdown');
+export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage breakdown');
export const TOTAL_USAGE_SUBTITLE = s__(
- 'UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items.',
+ 'UsageQuota|Includes artifacts, repositories, wiki, uploads, and other items.',
);
diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js
index 10668f08402..15796bc1870 100644
--- a/app/assets/javascripts/projects/storage_counter/index.js
+++ b/app/assets/javascripts/projects/storage_counter/index.js
@@ -25,7 +25,7 @@ export default (containerId = 'js-project-storage-count-app') => {
} = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js
index cb26603fff5..9fca9d88f46 100644
--- a/app/assets/javascripts/projects/storage_counter/utils.js
+++ b/app/assets/javascripts/projects/storage_counter/utils.js
@@ -14,10 +14,6 @@ export const parseGetProjectStorageResults = (data, helpLinks) => {
}
const { storageSize, ...storageStatistics } = projectStatistics;
const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
- if (!storageStatistics[currentType.id]) {
- return types;
- }
-
const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
const helpPath = helpLinks[helpPathKey];
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index f6f409873c8..a79da00de43 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -58,7 +58,7 @@ export default {
};
this.isLoading = false;
createFlash({
- message: s__('Something went wrong on our end'),
+ message: __('Something went wrong on our end'),
});
},
initPolling() {
diff --git a/app/assets/javascripts/projects/upload_file.js b/app/assets/javascripts/projects/upload_file.js
new file mode 100644
index 00000000000..597965eabfc
--- /dev/null
+++ b/app/assets/javascripts/projects/upload_file.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import createRouter from '~/repository/router';
+import UploadButton from './details/upload_button.vue';
+
+export const initUploadFileTrigger = () => {
+ const uploadFileTriggerEl = document.querySelector('.js-upload-file-trigger');
+
+ if (!uploadFileTriggerEl) return false;
+
+ const {
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ path,
+ projectPath,
+ } = uploadFileTriggerEl.dataset;
+
+ return new Vue({
+ el: uploadFileTriggerEl,
+ router: createRouter(projectPath, originalBranch),
+ provide: {
+ targetBranch,
+ originalBranch,
+ canPushCode: parseBoolean(canPushCode),
+ path,
+ projectPath,
+ },
+ render(h) {
+ return h(UploadButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file_experiment.js
deleted file mode 100644
index a7519f2bce8..00000000000
--- a/app/assets/javascripts/projects/upload_file_experiment.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import createRouter from '~/repository/router';
-import UploadButton from './details/upload_button.vue';
-
-export const initUploadFileTrigger = () => {
- const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger');
-
- if (!uploadFileTriggerEl) return false;
-
- const {
- targetBranch,
- originalBranch,
- canPushCode,
- path,
- projectPath,
- } = uploadFileTriggerEl.dataset;
-
- return new Vue({
- el: uploadFileTriggerEl,
- router: createRouter(projectPath, originalBranch),
- provide: {
- targetBranch,
- originalBranch,
- canPushCode: parseBoolean(canPushCode),
- path,
- projectPath,
- },
- render(h) {
- return h(UploadButton);
- },
- });
-};
diff --git a/app/assets/javascripts/projects/upload_file_experiment_tracking.js b/app/assets/javascripts/projects/upload_file_experiment_tracking.js
deleted file mode 100644
index c5e93f19b32..00000000000
--- a/app/assets/javascripts/projects/upload_file_experiment_tracking.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import ExperimentTracking from '~/experimentation/experiment_tracking';
-
-export const trackFileUploadEvent = (eventName) => {
- const isEmpty = Boolean(document.querySelector('.project-home-panel.empty-project'));
- const property = isEmpty ? 'empty' : 'nonempty';
- const label = 'blob-upload-modal';
- const FileUploadTracking = new ExperimentTracking('empty_repo_upload', { label, property });
- FileUploadTracking.event(eventName);
-};
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 1cef986a83d..397e3ed2ac8 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -15,9 +15,9 @@ export const DEFAULT_I18N = Object.freeze({
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
noResults: __('No matching results'),
- branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
+ branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
tagsErrorMessage: __('An error occurred while fetching tags. Retry the search.'),
- commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
+ commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
branches: __('Branches'),
tags: __('Tags'),
commits: __('Commits'),
diff --git a/app/assets/javascripts/registry/explorer/graphql/index.js b/app/assets/javascripts/registry/explorer/graphql/index.js
deleted file mode 100644
index d934bcc7419..00000000000
--- a/app/assets/javascripts/registry/explorer/graphql/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-
-Vue.use(VueApollo);
-
-export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- batchMax: 1,
- assumeImmutableResults: true,
- },
- ),
-});
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index 05858c7469d..50835142d28 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { sprintf, n__, s__ } from '~/locale';
+import { sprintf, __, n__ } from '~/locale';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
@@ -40,7 +40,7 @@ export default {
this.totalCount,
);
- return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText });
+ return sprintf(__('%{mrText}, this issue will be closed automatically.'), { mrText });
},
},
mounted() {
@@ -64,58 +64,51 @@ export default {
</script>
<template>
- <div
- v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"
- id="related-merge-requests"
- >
- <div id="merge-requests" class="card card-slim mt-3">
+ <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
+ <div class="card card-slim gl-mt-5">
<div class="card-header">
- <div class="card-title mt-0 mb-0 h5 merge-requests-title position-relative">
+ <div
+ class="card-title gl-relative gl-display-flex gl-align-items-center gl-line-height-20 gl-font-weight-bold gl-m-0"
+ >
<gl-link
- id="user-content-related-merge-requests"
- class="anchor position-absolute text-decoration-none"
+ class="anchor gl-absolute gl-text-decoration-none"
href="#related-merge-requests"
- aria-hidden="true"
+ aria-labelledby="related-merge-requests"
/>
- <span class="mr-1">
+ <h3 id="related-merge-requests" class="gl-font-base gl-m-0">
{{ __('Related merge requests') }}
- </span>
- <div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
- <div
- class="mr-count-badge gl-display-inline-flex gl-align-items-center gl-py-2 gl-px-3"
- >
- <svg class="s16 mr-1 text-secondary">
- <gl-icon name="merge-request" class="mr-1 text-secondary" />
- </svg>
- <span class="js-items-count">{{ totalCount }}</span>
- </div>
- </div>
+ </h3>
+ <template v-if="totalCount">
+ <gl-icon name="merge-request" class="gl-ml-5 gl-mr-2 gl-text-gray-500" />
+ <span data-testid="count">{{ totalCount }}</span>
+ </template>
</div>
</div>
- <div>
- <div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon">
- <gl-loading-icon size="sm" label="Fetching related merge requests" class="py-2" />
- </div>
- <ul v-else class="content-list related-items-list">
- <li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
- <related-issuable-item
- :id-key="mr.id"
- :display-reference="mr.reference"
- :title="mr.title"
- :milestone="mr.milestone"
- :assignees="getAssignees(mr)"
- :created-at="mr.created_at"
- :closed-at="mr.closed_at"
- :merged-at="mr.merged_at"
- :path="mr.web_url"
- :state="mr.state"
- :is-merge-request="true"
- :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
- path-id-separator="!"
- />
- </li>
- </ul>
- </div>
+ <gl-loading-icon
+ v-if="isFetchingMergeRequests"
+ size="sm"
+ label="Fetching related merge requests"
+ class="gl-py-3"
+ />
+ <ul v-else class="content-list related-items-list">
+ <li v-for="mr in mergeRequests" :key="mr.id" class="list-item gl-m-0! gl-p-0!">
+ <related-issuable-item
+ :id-key="mr.id"
+ :display-reference="mr.reference"
+ :title="mr.title"
+ :milestone="mr.milestone"
+ :assignees="getAssignees(mr)"
+ :created-at="mr.created_at"
+ :closed-at="mr.closed_at"
+ :merged-at="mr.merged_at"
+ :path="mr.web_url"
+ :state="mr.state"
+ :is-merge-request="true"
+ :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
+ path-id-separator="!"
+ />
+ </li>
+ </ul>
</div>
<div
v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js
index 652d03a0fd0..94abb50de89 100644
--- a/app/assets/javascripts/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/related_merge_requests/store/actions.js
@@ -1,7 +1,7 @@
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import * as types from './mutation_types';
const REQUEST_PAGE_COUNT = 100;
@@ -30,7 +30,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
.catch(() => {
dispatch('receiveDataError');
createFlash({
- message: s__('Something went wrong while fetching related merge requests.'),
+ message: __('Something went wrong while fetching related merge requests.'),
});
});
};
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 80f59485426..9e05d00a98d 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -95,6 +95,8 @@ export default {
noRefSelected: __('No tag selected'),
dropdownHeader: __('Tag name'),
searchPlaceholder: __('Search or create tag'),
+ label: __('Tag name'),
+ labelDescription: __('*Required'),
},
createFrom: {
noRefSelected: __('No source selected'),
@@ -108,11 +110,12 @@ export default {
<template>
<div>
<gl-form-group
- :label="__('Tag name')"
- :label-for="tagNameInputId"
data-testid="tag-name-field"
:state="!showTagNameValidationError"
:invalid-feedback="__('Tag name is required')"
+ :label="$options.translations.tagName.label"
+ :label-for="tagNameInputId"
+ :label-description="$options.translations.tagName.labelDescription"
>
<form-field-container>
<ref-selector
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index f2d89dbe682..c69481150e0 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -6,7 +6,7 @@
# When the `releases_index_apollo_client` feature flag is
# removed, this query should be removed entirely.
-query allReleases(
+query allReleasesDeprecated(
$fullPath: ID!
$first: Int
$last: Int
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 59f6ebfc928..86fa72d1496 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -24,7 +24,6 @@ export default () => {
// the purpose of making separate requests. So we explicitly
// disable batching on this page.
batchMax: 1,
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
index 686f9e294b7..7272880197a 100644
--- a/app/assets/javascripts/releases/mount_show.js
+++ b/app/assets/javascripts/releases/mount_show.js
@@ -6,12 +6,7 @@ import ReleaseShowApp from './components/app_show.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 7ad9fb56972..2cc5a8a79d2 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
@@ -17,12 +17,16 @@ import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
export default {
+ i18n: {
+ pipelineEditor: __('Pipeline Editor'),
+ },
components: {
BlobHeader,
BlobEdit,
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
+ GlButton,
ForkSuggestion,
},
mixins: [getRefMixin],
@@ -105,6 +109,7 @@ export default {
rawPath: '',
externalStorageUrl: '',
replacePath: '',
+ pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
@@ -242,6 +247,18 @@ export default {
:needs-to-fork="showForkSuggestion"
@edit="editBlob"
/>
+
+ <gl-button
+ v-if="blobInfo.pipelineEditorPath"
+ class="gl-mr-3"
+ category="secondary"
+ variant="confirm"
+ data-testid="pipeline-editor"
+ :href="blobInfo.pipelineEditorPath"
+ >
+ {{ $options.i18n.pipelineEditor }}
+ </gl-button>
+
<blob-button-group
v-if="isLoggedIn"
:path="path"
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index a307b7c0b8a..4a8cedb60b4 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -116,15 +116,14 @@ export default {
],
};
},
- /* eslint-disable dot-notation */
showCreateNewMrToggle() {
- return this.canPushCode && this.form.fields['branch_name'].value !== this.originalBranch;
+ return this.canPushCode && this.form.fields.branch_name.value !== this.originalBranch;
},
formCompleted() {
- return this.form.fields['commit_message'].value && this.form.fields['branch_name'].value;
+ return this.form.fields.commit_message.value && this.form.fields.branch_name.value;
},
showHint() {
- const splitCommitMessageByLineBreak = this.form.fields['commit_message'].value
+ const splitCommitMessageByLineBreak = this.form.fields.commit_message.value
.trim()
.split('\n');
const [firstLine, ...otherLines] = splitCommitMessageByLineBreak;
@@ -136,7 +135,7 @@ export default {
otherLines.some((text) => text.length > COMMIT_MESSAGE_BODY_MAX_LENGTH);
return (
- !this.form.fields['commit_message'].feedback &&
+ !this.form.fields.commit_message.feedback &&
(hasFirstLineExceedMaxLength || hasOtherLineExceedMaxLength)
);
},
@@ -173,9 +172,7 @@ export default {
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<template v-if="emptyRepo">
- <!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name'
- Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 -->
- <input type="hidden" name="branch_name" :value="originalBranch" />
+ <input type="hidden" name="branch_name" :value="originalBranch" class="js-branch-name" />
</template>
<template v-else>
<input type="hidden" name="original_branch" :value="originalBranch" />
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 5c713796bd6..62066973ee6 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,5 +1,12 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlTooltipDirective,
+ GlLink,
+ GlButton,
+ GlButtonGroup,
+ GlLoadingIcon,
+ GlSafeHtmlDirective,
+} from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
@@ -23,6 +30,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [getRefMixin],
apollo: {
@@ -96,6 +104,9 @@ export default {
},
},
defaultAvatarUrl,
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
};
</script>
@@ -121,10 +132,10 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link
+ v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.webPath"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
- v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */"
/>
<gl-button
v-if="commit.descriptionHtml"
@@ -150,15 +161,15 @@ export default {
</div>
<pre
v-if="commitDescription"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'd-block': showDescription }"
class="commit-row-description gl-mb-3"
- v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
<div class="commit-actions flex-row">
<div
v-if="commit.signatureHtml"
- v-html="commit.signatureHtml /* eslint-disable-line vue/no-v-html */"
+ v-safe-html:[$options.safeHtmlConfig]="commit.signatureHtml"
></div>
<div v-if="commit.pipeline" class="ci-status-link">
<gl-link
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 5010d60f374..bd06c064ab7 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -11,7 +11,6 @@ import {
GlIntersectionObserver,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE } from '~/repository/constants';
@@ -178,8 +177,7 @@ export default {
return this.isFolder ? this.loadFolder() : this.loadBlob();
},
loadFolder() {
- const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
- this.apolloQuery(query, {
+ this.apolloQuery(paginatedTreeQuery, {
projectPath: this.projectPath,
ref: this.ref,
path: this.path,
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 16dfe3cfb14..ffe8d5531f8 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,5 +1,4 @@
<script>
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -72,9 +71,6 @@ export default {
hasShowMore() {
return !this.clickedShowMore && this.pageLimitReached;
},
- paginatedTreeEnabled() {
- return this.glFeatures.paginatedTreeGraphqlQuery;
- },
},
watch: {
@@ -101,7 +97,7 @@ export default {
return this.$apollo
.query({
- query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
+ query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
@@ -114,20 +110,19 @@ export default {
if (data.errors) throw data.errors;
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
- const pageInfo = this.paginatedTreeEnabled
- ? data.project.repository.paginatedTree.pageInfo
- : this.hasNextPage(data.project.repository.tree);
+ const {
+ project: {
+ repository: {
+ paginatedTree: { pageInfo },
+ },
+ },
+ } = data;
this.isLoadingFiles = false;
this.entries = Object.keys(this.entries).reduce(
(acc, key) => ({
...acc,
- [key]: this.normalizeData(
- key,
- this.paginatedTreeEnabled
- ? data.project.repository.paginatedTree.nodes[0][key]
- : data.project.repository.tree[key].edges,
- ),
+ [key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]),
}),
{},
);
@@ -149,9 +144,7 @@ export default {
});
},
normalizeData(key, data) {
- return this.entries[key].concat(
- this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
- );
+ return this.entries[key].concat(data.nodes);
},
hasNextPage(data) {
return []
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 0199b893453..11e5b5608cb 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -15,7 +15,6 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
SECONDARY_OPTIONS_TEXT,
@@ -165,9 +164,6 @@ export default {
},
})
.then((response) => {
- if (!this.replacePath) {
- trackFileUploadEvent('click_upload_modal_form_submit');
- }
visitUrl(response.data.filePath);
})
.catch(() => {
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 4892e54ebef..96d712ce9b4 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -64,7 +64,6 @@ const defaultClient = createDefaultClient(
/* eslint-enable @gitlab/require-i18n-strings */
},
},
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index a2ddcbf0e4c..30c36dee48f 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,4 +1,3 @@
-import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import getRefMixin from './get_ref';
@@ -22,7 +21,7 @@ export default {
return this.$apollo
.query({
- query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
+ query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 8e0b5e21ca3..cf3892802fd 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -31,6 +31,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
storedExternally
rawPath
replacePath
+ pipelineEditorPath
simpleViewer {
fileType
tooLarge
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 29642b6633f..48a15954035 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -3,6 +3,7 @@ export * from './api/projects_api';
export * from './api/user_api';
export * from './api/markdown_api';
export * from './api/bulk_imports_api';
+export * from './api/namespaces_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 381421cdc23..3c8533dd06d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import { hide, fixTitle } from '~/tooltips';
+import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
@@ -130,8 +131,10 @@ Sidebar.prototype.openDropdown = function (blockOrName) {
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout(() => {
- $block.find('.js-sidebar-dropdown-toggle').trigger('click');
- });
+ if (!gon.features?.labelsWidget && !$block.hasClass('labels-select-wrapper')) {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ }
+ }, DEBOUNCE_DROPDOWN_DELAY);
};
Sidebar.prototype.setCollapseAfterUpdate = function ($block) {
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index c8513a0b803..3edb658eaf5 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -1,18 +1,26 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
-import { formatNumber, sprintf, __ } from '~/locale';
+import { sprintf, __ } from '~/locale';
+
+import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
-import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
+import RunnerTypeTabs from '../components/runner_type_tabs.vue';
+
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
-import { typeTokenConfig } from '../components/search_tokens/type_token_config';
-import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_FETCH_ERROR,
+} from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
@@ -24,19 +32,37 @@ import { captureException } from '../sentry_utils';
export default {
name: 'AdminRunnersApp',
components: {
+ GlBadge,
GlLink,
+ RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
- RunnerManualSetupHelp,
RunnerName,
RunnerPagination,
+ RunnerTypeTabs,
},
props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
activeRunnersCount: {
- type: Number,
+ type: String,
required: true,
},
- registrationToken: {
+ allRunnersCount: {
+ type: String,
+ required: true,
+ },
+ instanceRunnersCount: {
+ type: String,
+ required: true,
+ },
+ groupRunnersCount: {
+ type: String,
+ required: true,
+ },
+ projectRunnersCount: {
type: String,
required: true,
},
@@ -86,13 +112,12 @@ export default {
},
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: formatNumber(this.activeRunnersCount),
+ active_runners_count: this.activeRunnersCount,
});
},
searchTokens() {
return [
statusTokenConfig,
- typeTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
@@ -116,6 +141,20 @@ export default {
this.reportToSentry(error);
},
methods: {
+ tabCount({ runnerType }) {
+ switch (runnerType) {
+ case null:
+ return this.allRunnersCount;
+ case INSTANCE_TYPE:
+ return this.instanceRunnersCount;
+ case GROUP_TYPE:
+ return this.groupRunnersCount;
+ case PROJECT_TYPE:
+ return this.projectRunnersCount;
+ default:
+ return null;
+ }
+ },
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
@@ -126,10 +165,30 @@ export default {
</script>
<template>
<div>
- <runner-manual-setup-help
- :registration-token="registrationToken"
- :type="$options.INSTANCE_TYPE"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
+ >
+ <runner-type-tabs
+ v-model="search"
+ class="gl-w-full"
+ content-class="gl-display-none"
+ nav-class="gl-border-none!"
+ >
+ <template #title="{ tab }">
+ {{ tab.title }}
+ <gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
+ {{ tabCount(tab) }}
+ </gl-badge>
+ </template>
+ </runner-type-tabs>
+
+ <registration-dropdown
+ class="gl-w-full gl-sm-w-auto gl-mr-auto"
+ :registration-token="registrationToken"
+ :type="$options.INSTANCE_TYPE"
+ right
+ />
+ </div>
<runner-filtered-search-bar
v-model="search"
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 1eec1019b73..62da6cbfa2b 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AdminRunnersApp from './admin_runners_app.vue';
+Vue.use(GlToast);
Vue.use(VueApollo);
export const initAdminRunners = (selector = '#js-admin-runners') => {
@@ -14,15 +16,19 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
// TODO `activeRunnersCount` should be implemented using a GraphQL API
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806
- const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset;
+ const {
+ runnerInstallHelpPage,
+ registrationToken,
+
+ activeRunnersCount,
+ allRunnersCount,
+ instanceRunnersCount,
+ groupRunnersCount,
+ projectRunnersCount,
+ } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
@@ -34,8 +40,15 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
render(h) {
return h(AdminRunnersApp, {
props: {
- activeRunnersCount: parseInt(activeRunnersCount, 10),
registrationToken,
+
+ // All runner counts are returned as formatted
+ // strings, we do not use `parseInt`.
+ activeRunnersCount,
+ allRunnersCount,
+ instanceRunnersCount,
+ groupRunnersCount,
+ projectRunnersCount,
},
});
},
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index e26bdbf1aea..c4bddb7b398 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -3,7 +3,7 @@ import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
const i18n = {
@@ -71,7 +71,7 @@ export default {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
- mutation: runnerUpdateMutation,
+ mutation: runnerActionsUpdateMutation,
variables: {
input: {
id: this.runner.id,
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
new file mode 100644
index 00000000000..9ba1192bc8c
--- /dev/null
+++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+
+import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue';
+import RunnerPausedBadge from '../runner_paused_badge.vue';
+
+import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
+
+export default {
+ components: {
+ RunnerContactedStateBadge,
+ RunnerPausedBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ paused() {
+ return !this.runner.active;
+ },
+ },
+ i18n: {
+ I18N_LOCKED_RUNNER_DESCRIPTION,
+ I18N_PAUSED_RUNNER_DESCRIPTION,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <runner-contacted-state-badge :runner="runner" size="sm" />
+ <runner-paused-badge v-if="paused" size="sm" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
index 886b5cb29fc..3b476997915 100644
--- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
@@ -1,11 +1,21 @@
<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import RunnerName from '../runner_name.vue';
+import RunnerTypeBadge from '../runner_type_badge.vue';
+
+import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants';
export default {
components: {
+ GlIcon,
TooltipOnTruncate,
RunnerName,
+ RunnerTypeBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
runner: {
@@ -14,10 +24,19 @@ export default {
},
},
computed: {
+ runnerType() {
+ return this.runner.runnerType;
+ },
+ locked() {
+ return this.runner.locked;
+ },
description() {
return this.runner.description;
},
},
+ i18n: {
+ I18N_LOCKED_RUNNER_DESCRIPTION,
+ },
};
</script>
@@ -26,6 +45,14 @@ export default {
<slot :runner="runner" name="runner-name">
<runner-name :runner="runner" />
</slot>
+
+ <runner-type-badge :type="runnerType" size="sm" />
+ <gl-icon
+ v-if="locked"
+ v-gl-tooltip
+ :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
+ name="lock"
+ />
<tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child">
<div class="gl-text-truncate">
{{ description }}
diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
deleted file mode 100644
index c8cb0bf6088..00000000000
--- a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import RunnerTypeBadge from '../runner_type_badge.vue';
-import RunnerStateLockedBadge from '../runner_state_locked_badge.vue';
-import RunnerStatePausedBadge from '../runner_state_paused_badge.vue';
-import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
-
-export default {
- components: {
- RunnerTypeBadge,
- RunnerStateLockedBadge,
- RunnerStatePausedBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- runner: {
- type: Object,
- required: true,
- },
- },
- computed: {
- runnerType() {
- return this.runner.runnerType;
- },
- locked() {
- return this.runner.locked;
- },
- paused() {
- return !this.runner.active;
- },
- },
- i18n: {
- I18N_LOCKED_RUNNER_DESCRIPTION,
- I18N_PAUSED_RUNNER_DESCRIPTION,
- },
-};
-</script>
-
-<template>
- <div>
- <runner-type-badge :type="runnerType" size="sm" />
- <runner-state-locked-badge v-if="locked" size="sm" />
- <runner-state-paused-badge v-if="paused" size="sm" />
- </div>
-</template>
diff --git a/app/assets/javascripts/runner/components/helpers/masked_value.vue b/app/assets/javascripts/runner/components/helpers/masked_value.vue
deleted file mode 100644
index feccb37de81..00000000000
--- a/app/assets/javascripts/runner/components/helpers/masked_value.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlButton,
- },
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- isMasked: true,
- };
- },
- computed: {
- label() {
- if (this.isMasked) {
- return __('Click to reveal');
- }
- return __('Click to hide');
- },
- icon() {
- if (this.isMasked) {
- return 'eye';
- }
- return 'eye-slash';
- },
- displayedValue() {
- if (this.isMasked && this.value?.length) {
- return '*'.repeat(this.value.length);
- }
- return this.value;
- },
- },
- methods: {
- toggleMasked() {
- this.isMasked = !this.isMasked;
- },
- },
-};
-</script>
-<template>
- <span
- >{{ displayedValue }}
- <gl-button
- :aria-label="label"
- :icon="icon"
- class="gl-text-body!"
- data-testid="toggle-masked"
- variant="link"
- @click="toggleMasked"
- />
- </span>
-</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
new file mode 100644
index 00000000000..3fbe3c1be74
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
@@ -0,0 +1,112 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
+import RegistrationToken from './registration_token.vue';
+import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue';
+
+export default {
+ i18n: {
+ showInstallationInstructions: s__(
+ 'Runners|Show runner installation and registration instructions',
+ ),
+ registrationToken: s__('Runners|Registration token'),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlDropdownDivider,
+ RegistrationToken,
+ RunnerInstructionsModal,
+ RegistrationTokenResetDropdownItem,
+ },
+ props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
+ },
+ },
+ data() {
+ return {
+ currentRegistrationToken: this.registrationToken,
+ instructionsModalOpened: false,
+ };
+ },
+ computed: {
+ dropdownText() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return s__('Runners|Register an instance runner');
+ case GROUP_TYPE:
+ return s__('Runners|Register a group runner');
+ case PROJECT_TYPE:
+ return s__('Runners|Register a project runner');
+ default:
+ return s__('Runners|Register a runner');
+ }
+ },
+ },
+ methods: {
+ onShowInstructionsClick() {
+ // Rendering the modal on demand, to avoid
+ // loading instructions prematurely from API.
+ this.instructionsModalOpened = true;
+
+ this.$nextTick(() => {
+ // $refs.runnerInstructionsModal is defined in
+ // the tick after the modal is rendered
+ this.$refs.runnerInstructionsModal.show();
+ });
+ },
+ onTokenReset(token) {
+ this.currentRegistrationToken = token;
+
+ this.$refs.runnerRegistrationDropdown.hide(true);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="runnerRegistrationDropdown"
+ menu-class="gl-w-auto!"
+ :text="dropdownText"
+ variant="confirm"
+ v-bind="$attrs"
+ >
+ <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
+ {{ $options.i18n.showInstallationInstructions }}
+ <runner-instructions-modal
+ v-if="instructionsModalOpened"
+ ref="runnerInstructionsModal"
+ :registration-token="registrationToken"
+ data-testid="runner-instructions-modal"
+ />
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-form class="gl-p-4!">
+ <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken">
+ <registration-token :value="currentRegistrationToken" />
+ </gl-form-group>
+ </gl-dropdown-form>
+ <gl-dropdown-divider />
+ <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" />
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue
new file mode 100644
index 00000000000..d54a66ff0e4
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_token.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ components: {
+ GlButtonGroup,
+ GlButton,
+ ModalCopyButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isMasked: true,
+ };
+ },
+ computed: {
+ maskLabel() {
+ if (this.isMasked) {
+ return __('Click to reveal');
+ }
+ return __('Click to hide');
+ },
+ maskIcon() {
+ if (this.isMasked) {
+ return 'eye';
+ }
+ return 'eye-slash';
+ },
+ displayedValue() {
+ if (this.isMasked && this.value?.length) {
+ return '*'.repeat(this.value.length);
+ }
+ return this.value;
+ },
+ },
+ methods: {
+ onToggleMasked() {
+ this.isMasked = !this.isMasked;
+ },
+ onCopied() {
+ // value already in the clipboard, simply notify the user
+ this.$toast?.show(s__('Runners|Registration token copied!'));
+ },
+ },
+ i18n: {
+ copyLabel: s__('Runners|Copy registration token'),
+ },
+};
+</script>
+<template>
+ <gl-button-group>
+ <gl-button class="gl-font-monospace" data-testid="token-value" label>
+ {{ displayedValue }}
+ </gl-button>
+ <gl-button
+ v-gl-tooltip
+ :aria-label="maskLabel"
+ :title="maskLabel"
+ :icon="maskIcon"
+ class="gl-w-auto! gl-flex-shrink-0!"
+ data-testid="toggle-masked"
+ @click.stop="onToggleMasked"
+ />
+ <modal-copy-button
+ class="gl-w-auto! gl-flex-shrink-0!"
+ :aria-label="$options.i18n.copyLabel"
+ :title="$options.i18n.copyLabel"
+ :text="value"
+ @success="onCopied"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
new file mode 100644
index 00000000000..3bb15bff8d8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { __, s__ } from '~/locale';
+import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
+
+export default {
+ name: 'RunnerRegistrationTokenReset',
+ components: {
+ GlDropdownItem,
+ GlLoadingIcon,
+ },
+ inject: {
+ groupId: {
+ default: null,
+ },
+ projectId: {
+ default: null,
+ },
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ resetTokenInput() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return {
+ type: this.type,
+ };
+ case GROUP_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_GROUP, this.groupId),
+ type: this.type,
+ };
+ case PROJECT_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
+ type: this.type,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+ methods: {
+ async resetToken() {
+ // TODO Replace confirmation with gl-modal
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
+ return;
+ }
+
+ this.loading = true;
+ try {
+ const {
+ data: {
+ runnersRegistrationTokenReset: { token, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: runnersRegistrationTokenResetMutation,
+ variables: {
+ input: this.resetTokenInput,
+ },
+ });
+ if (errors && errors.length) {
+ throw new Error(errors.join(' '));
+ }
+ this.onSuccess(token);
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.loading = false;
+ }
+ },
+ onError(error) {
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
+ },
+ onSuccess(token) {
+ this.$toast?.show(s__('Runners|New registration token generated!'));
+ this.$emit('tokenReset', token);
+ },
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown-item @click.capture.native.stop="resetToken">
+ {{ __('Reset registration token') }}
+ <gl-loading-icon v-if="loading" inline />
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue
new file mode 100644
index 00000000000..b4727f832f8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import {
+ I18N_ONLINE_RUNNER_DESCRIPTION,
+ I18N_OFFLINE_RUNNER_DESCRIPTION,
+ I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_NOT_CONNECTED,
+} from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ contactedAtTimeAgo() {
+ if (this.runner.contactedAt) {
+ return getTimeago().format(this.runner.contactedAt);
+ }
+ return null;
+ },
+ badge() {
+ switch (this.runner.status) {
+ case STATUS_ONLINE:
+ return {
+ variant: 'success',
+ label: s__('Runners|online'),
+ tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, {
+ timeAgo: this.contactedAtTimeAgo,
+ }),
+ };
+ case STATUS_OFFLINE:
+ return {
+ variant: 'muted',
+ label: s__('Runners|offline'),
+ tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, {
+ timeAgo: this.contactedAtTimeAgo,
+ }),
+ };
+ case STATUS_NOT_CONNECTED:
+ return {
+ variant: 'muted',
+ label: s__('Runners|not connected'),
+ tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
+ {{ badge.label }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index e04ca8ddca0..a9dfec35479 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -2,6 +2,7 @@
import { cloneDeep } from 'lodash';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { searchValidator } from '~/runner/runner_search_utils';
import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
@@ -31,9 +32,12 @@ export default {
value: {
type: Object,
required: true,
- validator(val) {
- return Array.isArray(val?.filters) && typeof val?.sort === 'string';
- },
+ validator: searchValidator,
+ },
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
},
namespace: {
type: String,
@@ -43,7 +47,7 @@ export default {
data() {
// filtered_search_bar_root.vue may mutate the inital
// filters. Use `cloneDeep` to prevent those mutations
- // from affecting this component
+ // from affecting this component
const { filters, sort } = cloneDeep(this.value);
return {
initialFilterValue: filters,
@@ -52,19 +56,17 @@ export default {
},
methods: {
onFilter(filters) {
- const { sort } = this.value;
-
+ // Apply new filters, from page 1
this.$emit('input', {
+ ...this.value,
filters,
- sort,
pagination: { page: 1 },
});
},
onSort(sort) {
- const { filters } = this.value;
-
+ // Apply new sort, from page 1
this.$emit('input', {
- filters,
+ ...this.value,
sort,
pagination: { page: 1 },
});
@@ -74,13 +76,16 @@ export default {
};
</script>
<template>
- <div>
+ <div
+ class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
+ >
<filtered-search
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
+ :tokens="tokens"
:initial-sort-by="initialSortBy"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 3f6ea389288..f8dbc469c22 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { formatNumber, __, s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
-import RunnerTypeCell from './cells/runner_type_cell.vue';
+import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
const tableField = ({ key, label = '', width = 10 }) => {
@@ -37,7 +36,7 @@ export default {
RunnerActionsCell,
RunnerSummaryCell,
RunnerTags,
- RunnerTypeCell,
+ RunnerStatusCell,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -54,18 +53,6 @@ export default {
},
},
methods: {
- formatProjectCount(projectCount) {
- if (projectCount === null) {
- return __('n/a');
- }
- return formatNumber(projectCount);
- },
- formatJobCount(jobCount) {
- if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
- return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
- }
- return formatNumber(jobCount);
- },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -76,13 +63,11 @@ export default {
},
},
fields: [
- tableField({ key: 'type', label: __('Type/State') }),
- tableField({ key: 'summary', label: s__('Runners|Runner'), width: 30 }),
+ tableField({ key: 'status', label: s__('Runners|Status') }),
+ tableField({ key: 'summary', label: s__('Runners|Runner ID'), width: 30 }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'ipAddress', label: __('IP Address') }),
- tableField({ key: 'projectCount', label: __('Projects'), width: 5 }),
- tableField({ key: 'jobCount', label: __('Jobs'), width: 5 }),
- tableField({ key: 'tagList', label: __('Tags') }),
+ tableField({ key: 'tagList', label: __('Tags'), width: 20 }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
],
@@ -103,8 +88,8 @@ export default {
<gl-skeleton-loader v-for="i in 4" :key="i" />
</template>
- <template #cell(type)="{ item }">
- <runner-type-cell :runner="item" />
+ <template #cell(status)="{ item }">
+ <runner-status-cell :runner="item" />
</template>
<template #cell(summary)="{ item, index }">
@@ -123,14 +108,6 @@ export default {
{{ ipAddress }}
</template>
- <template #cell(projectCount)="{ item: { projectCount } }">
- {{ formatProjectCount(projectCount) }}
- </template>
-
- <template #cell(jobCount)="{ item: { jobCount } }">
- {{ formatJobCount(jobCount) }}
- </template>
-
<template #cell(tagList)="{ item: { tagList } }">
<runner-tags :tag-list="tagList" size="sm" />
</template>
diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
deleted file mode 100644
index 475d362bb52..00000000000
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
-import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-
-export default {
- components: {
- GlLink,
- GlSprintf,
- ClipboardButton,
- MaskedValue,
- RunnerInstructions,
- RunnerRegistrationTokenReset,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: {
- runnerInstallHelpPage: {
- default: null,
- },
- },
- props: {
- registrationToken: {
- type: String,
- required: true,
- },
- type: {
- type: String,
- required: true,
- validator(type) {
- return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
- },
- },
- },
- data() {
- return {
- currentRegistrationToken: this.registrationToken,
- };
- },
- computed: {
- rootUrl() {
- return gon.gitlab_url || '';
- },
- typeName() {
- switch (this.type) {
- case INSTANCE_TYPE:
- return s__('Runners|shared');
- case GROUP_TYPE:
- return s__('Runners|group');
- case PROJECT_TYPE:
- return s__('Runners|specific');
- default:
- return '';
- }
- },
- },
- methods: {
- onTokenReset(token) {
- this.currentRegistrationToken = token;
- },
- },
-};
-</script>
-
-<template>
- <div class="bs-callout">
- <h5 data-testid="runner-help-title">
- <gl-sprintf :message="__('Set up a %{type} runner manually')">
- <template #type>
- {{ typeName }}
- </template>
- </gl-sprintf>
- </h5>
-
- <ol>
- <li>
- <gl-link :href="runnerInstallHelpPage" data-testid="runner-help-link" target="_blank">
- {{ __("Install GitLab Runner and ensure it's running.") }}
- </gl-link>
- </li>
- <li>
- {{ __('Register the runner with this URL:') }}
- <br />
-
- <code data-testid="coordinator-url">{{ rootUrl }}</code>
- <clipboard-button :title="__('Copy URL')" :text="rootUrl" />
- </li>
- <li>
- {{ __('And this registration token:') }}
- <br />
-
- <code data-testid="registration-token"
- ><masked-value :value="currentRegistrationToken"
- /></code>
- <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
- </li>
- </ol>
-
- <runner-registration-token-reset :type="type" @tokenReset="onTokenReset" />
-
- <runner-instructions />
- </div>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue b/app/assets/javascripts/runner/components/runner_paused_badge.vue
index d1e6fa05e4d..d1e6fa05e4d 100644
--- a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_paused_badge.vue
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
deleted file mode 100644
index cdf14abd4f9..00000000000
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import createFlash, { FLASH_TYPES } from '~/flash';
-import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { __, s__ } from '~/locale';
-import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-
-export default {
- name: 'RunnerRegistrationTokenReset',
- components: {
- GlButton,
- },
- inject: {
- groupId: {
- default: null,
- },
- projectId: {
- default: null,
- },
- },
- props: {
- type: {
- type: String,
- required: true,
- validator(type) {
- return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
- },
- },
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- resetTokenInput() {
- switch (this.type) {
- case INSTANCE_TYPE:
- return {
- type: this.type,
- };
- case GROUP_TYPE:
- return {
- id: convertToGraphQLId(TYPE_GROUP, this.groupId),
- type: this.type,
- };
- case PROJECT_TYPE:
- return {
- id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
- type: this.type,
- };
- default:
- return null;
- }
- },
- },
- methods: {
- async resetToken() {
- // TODO Replace confirmation with gl-modal
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
- // eslint-disable-next-line no-alert
- if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
- return;
- }
-
- this.loading = true;
- try {
- const {
- data: {
- runnersRegistrationTokenReset: { token, errors },
- },
- } = await this.$apollo.mutate({
- mutation: runnersRegistrationTokenResetMutation,
- variables: {
- input: this.resetTokenInput,
- },
- });
- if (errors && errors.length) {
- throw new Error(errors.join(' '));
- }
- this.onSuccess(token);
- } catch (e) {
- this.onError(e);
- } finally {
- this.loading = false;
- }
- },
- onError(error) {
- const { message } = error;
- createFlash({ message });
-
- this.reportToSentry(error);
- },
- onSuccess(token) {
- createFlash({
- message: s__('Runners|New registration token generated!'),
- type: FLASH_TYPES.SUCCESS,
- });
- this.$emit('tokenReset', token);
- },
- reportToSentry(error) {
- captureException({ error, component: this.$options.name });
- },
- },
-};
-</script>
-<template>
- <gl-button :loading="loading" @click="resetToken">
- {{ __('Reset registration token') }}
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue b/app/assets/javascripts/runner/components/runner_state_locked_badge.vue
deleted file mode 100644
index 458526010bc..00000000000
--- a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
-
-export default {
- components: {
- GlBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- i18n: {
- I18N_LOCKED_RUNNER_DESCRIPTION,
- },
-};
-</script>
-<template>
- <gl-badge
- v-gl-tooltip="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
- variant="warning"
- v-bind="$attrs"
- >
- {{ s__('Runners|locked') }}
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue
index 06562e618a8..6ad2023a866 100644
--- a/app/assets/javascripts/runner/components/runner_tag.vue
+++ b/app/assets/javascripts/runner/components/runner_tag.vue
@@ -1,11 +1,15 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
export default {
components: {
GlBadge,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserver: GlResizeObserverDirective,
+ },
props: {
tag: {
type: String,
@@ -14,14 +18,39 @@ export default {
size: {
type: String,
required: false,
- default: 'md',
+ default: 'sm',
+ },
+ },
+ data() {
+ return {
+ overflowing: false,
+ };
+ },
+ computed: {
+ tooltip() {
+ if (this.overflowing) {
+ return this.tag;
+ }
+ return '';
+ },
+ },
+ methods: {
+ onResize() {
+ const { scrollWidth, offsetWidth } = this.$el;
+ this.overflowing = scrollWidth > offsetWidth;
},
},
RUNNER_TAG_BADGE_VARIANT,
};
</script>
<template>
- <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
+ <gl-badge
+ v-gl-tooltip="tooltip"
+ v-gl-resize-observer="onResize"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ :size="size"
+ :variant="$options.RUNNER_TAG_BADGE_VARIANT"
+ >
{{ tag }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue
index aec0d8e2c66..8da5e33076f 100644
--- a/app/assets/javascripts/runner/components/runner_tags.vue
+++ b/app/assets/javascripts/runner/components/runner_tags.vue
@@ -14,13 +14,19 @@ export default {
size: {
type: String,
required: false,
- default: 'md',
+ default: 'sm',
},
},
};
</script>
<template>
<div>
- <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
+ <runner-tag
+ v-for="tag in tagList"
+ :key="tag"
+ class="gl-display-inline gl-mr-1"
+ :tag="tag"
+ :size="size"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue
index aa435aaa823..1400875a1d6 100644
--- a/app/assets/javascripts/runner/components/runner_type_alert.vue
+++ b/app/assets/javascripts/runner/components/runner_type_alert.vue
@@ -9,17 +9,14 @@ const ALERT_DATA = {
message: s__(
'Runners|This runner is available to all groups and projects in your GitLab instance.',
),
- variant: 'success',
anchor: 'shared-runners',
},
[GROUP_TYPE]: {
message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
- variant: 'success',
anchor: 'group-runners',
},
[PROJECT_TYPE]: {
message: s__('Runners|This runner is associated with one or more projects.'),
- variant: 'info',
anchor: 'specific-runners',
},
};
@@ -50,7 +47,7 @@ export default {
};
</script>
<template>
- <gl-alert v-if="alert" :variant="alert.variant" :dismissible="false">
+ <gl-alert v-if="alert" variant="info" :dismissible="false">
{{ alert.message }}
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
</gl-alert>
diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue
index 1a61b80184b..b885dcefdcb 100644
--- a/app/assets/javascripts/runner/components/runner_type_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_type_badge.vue
@@ -12,17 +12,14 @@ import {
const BADGE_DATA = {
[INSTANCE_TYPE]: {
- variant: 'success',
text: s__('Runners|shared'),
tooltip: I18N_INSTANCE_RUNNER_DESCRIPTION,
},
[GROUP_TYPE]: {
- variant: 'success',
text: s__('Runners|group'),
tooltip: I18N_GROUP_RUNNER_DESCRIPTION,
},
[PROJECT_TYPE]: {
- variant: 'info',
text: s__('Runners|specific'),
tooltip: I18N_PROJECT_RUNNER_DESCRIPTION,
},
@@ -53,7 +50,7 @@ export default {
};
</script>
<template>
- <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
+ <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" variant="info" v-bind="$attrs">
{{ badge.text }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/runner/components/runner_type_tabs.vue
new file mode 100644
index 00000000000..b767dafaccf
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_type_tabs.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { searchValidator } from '~/runner/runner_search_utils';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+
+const tabs = [
+ {
+ title: s__('Runners|All'),
+ runnerType: null,
+ },
+ {
+ title: s__('Runners|Instance'),
+ runnerType: INSTANCE_TYPE,
+ },
+ {
+ title: s__('Runners|Group'),
+ runnerType: GROUP_TYPE,
+ },
+ {
+ title: s__('Runners|Project'),
+ runnerType: PROJECT_TYPE,
+ },
+];
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ },
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ validator: searchValidator,
+ },
+ },
+ methods: {
+ onTabSelected({ runnerType }) {
+ this.$emit('input', {
+ ...this.value,
+ runnerType,
+ pagination: { page: 1 },
+ });
+ },
+ isTabActive({ runnerType }) {
+ return runnerType === this.value.runnerType;
+ },
+ },
+ tabs,
+};
+</script>
+<template>
+ <gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
+ <gl-tab
+ v-for="tab in $options.tabs"
+ :key="`${tab.runnerType}`"
+ :active="isTabActive(tab)"
+ @click="onTabSelected(tab)"
+ >
+ <template #title>
+ <slot name="title" :tab="tab">{{ tab.title }}</slot>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
index 03dff5e61a5..9963048ae1d 100644
--- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -10,23 +10,29 @@ import {
PARAM_KEY_STATUS,
} from '../../constants';
+const options = [
+ { value: STATUS_ACTIVE, title: s__('Runners|Active') },
+ { value: STATUS_PAUSED, title: s__('Runners|Paused') },
+ { value: STATUS_ONLINE, title: s__('Runners|Online') },
+ { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
+ { value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') },
+];
+
export const statusTokenConfig = {
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
- options: [
- { value: STATUS_ACTIVE, title: s__('Runners|Active') },
- { value: STATUS_PAUSED, title: s__('Runners|Paused') },
- { value: STATUS_ONLINE, title: s__('Runners|Online') },
- { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
-
- // Added extra quotes in this title to avoid splitting this value:
+ options: options.map(({ value, title }) => ({
+ value,
+ // Replace whitespace with a special character to avoid
+ // splitting this value.
+ // Replacing in each option, as translations may also
+ // contain spaces!
+ // see: https://gitlab.com/gitlab-org/gitlab/-/issues/344142
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
- { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
- ],
- // TODO In principle we could support more complex search rules,
- // this can be added to a separate issue.
+ title: title.replace(' ', '\u00a0'),
+ })),
operators: OPERATOR_IS_ONLY,
};
diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
deleted file mode 100644
index 1da61c53386..00000000000
--- a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
-
-export const typeTokenConfig = {
- icon: 'file-tree',
- title: __('Type'),
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- unique: true,
- options: [
- { value: INSTANCE_TYPE, title: s__('Runners|instance') },
- { value: GROUP_TYPE, title: s__('Runners|group') },
- { value: PROJECT_TYPE, title: s__('Runners|project') },
- ],
- // TODO We should support more complex search rules,
- // search for multiple states (OR) or have NOT operators
- operators: OPERATOR_IS_ONLY,
-};
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index a2fb9d9efd8..3952e2398e0 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,21 +1,33 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
-export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
+// Type
export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects');
export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
'Runners|Available to all projects and subgroups in the group',
);
export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects');
+
+// Status
+export const I18N_ONLINE_RUNNER_DESCRIPTION = s__(
+ 'Runners|Runner is online; last contact was %{timeAgo}',
+);
+export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__(
+ 'Runners|No recent contact from this runner; last contact was %{timeAgo}',
+);
+export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
+ 'Runners|This runner has never connected to this instance',
+);
+
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
-export const RUNNER_TAG_BADGE_VARIANT = 'info';
+export const RUNNER_TAG_BADGE_VARIANT = 'neutral';
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names
diff --git a/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
new file mode 100644
index 00000000000..547cc43907c
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
@@ -0,0 +1,14 @@
+#import "~/runner/graphql/runner_node.fragment.graphql"
+
+# Mutation for updates within the runners list via action
+# buttons (play, pause, ...), loads attributes shown in the
+# runner list.
+
+mutation runnerActionsUpdate($input: RunnerUpdateInput!) {
+ runnerUpdate(input: $input) {
+ runner {
+ ...RunnerNode
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 68d6f02f799..98f2dab26ca 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -10,6 +10,5 @@ fragment RunnerNode on CiRunner {
locked
tagList
contactedAt
- jobCount
- projectCount
+ status
}
diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
index dcc7fdf24f1..ea622fd4958 100644
--- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
@@ -1,5 +1,8 @@
#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
+# Mutation for updates from the runner form, loads
+# attributes shown in the runner details.
+
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
runner {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 4bb28796dfa..c3dfa885f27 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -5,14 +5,14 @@ import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
+import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
-import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
+import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
-import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
@@ -31,11 +31,12 @@ export default {
name: 'GroupRunnersApp',
components: {
GlLink,
+ RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
- RunnerManualSetupHelp,
RunnerName,
RunnerPagination,
+ RunnerTypeTabs,
},
props: {
registrationToken: {
@@ -112,7 +113,7 @@ export default {
});
},
searchTokens() {
- return [statusTokenConfig, typeTokenConfig];
+ return [statusTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
@@ -144,7 +145,20 @@ export default {
<template>
<div>
- <runner-manual-setup-help :registration-token="registrationToken" :type="$options.GROUP_TYPE" />
+ <div class="gl-display-flex gl-align-items-center">
+ <runner-type-tabs
+ v-model="search"
+ content-class="gl-display-none"
+ nav-class="gl-border-none!"
+ />
+
+ <registration-dropdown
+ class="gl-ml-auto"
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ right
+ />
+ </div>
<runner-filtered-search-bar
v-model="search"
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index 9545764c68d..60b7a7ab541 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import GroupRunnersApp from './group_runners_app.vue';
+Vue.use(GlToast);
Vue.use(VueApollo);
export const initGroupRunners = (selector = '#js-group-runners') => {
@@ -21,12 +23,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
} = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/runner_details/index.js
index 05e6f86869d..db8f239a3c3 100644
--- a/app/assets/javascripts/runner/runner_details/index.js
+++ b/app/assets/javascripts/runner/runner_details/index.js
@@ -15,12 +15,7 @@ export const initRunnerDetail = (selector = '#js-runner-details') => {
const { runnerId } = el.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 0a817ea0acf..b88023720e8 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -18,6 +18,50 @@ import {
RUNNER_PAGE_SIZE,
} from './constants';
+/**
+ * The filters and sorting of the runners are built around
+ * an object called "search" that contains the current state
+ * of search in the UI. For example:
+ *
+ * ```
+ * const search = {
+ * // The current tab
+ * runnerType: 'INSTANCE_TYPE',
+ *
+ * // Filters in the search bar
+ * filters: [
+ * { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ * { type: 'filtered-search-term', value: { data: '' } },
+ * ],
+ *
+ * // Current sorting value
+ * sort: 'CREATED_DESC',
+ *
+ * // Pagination information
+ * pagination: { page: 1 },
+ * };
+ * ```
+ *
+ * An object in this format can be used to generate URLs
+ * with the search parameters or by runner components
+ * a input using a v-model.
+ *
+ * @module runner_search_utils
+ */
+
+/**
+ * Validates a search value
+ * @param {Object} search
+ * @returns {boolean} True if the value follows the search format.
+ */
+export const searchValidator = ({ runnerType, filters, sort }) => {
+ return (
+ (runnerType === null || typeof runnerType === 'string') &&
+ Array.isArray(filters) &&
+ typeof sort === 'string'
+ );
+};
+
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
const after = params[PARAM_KEY_AFTER];
@@ -35,13 +79,20 @@ const getPaginationFromParams = (params) => {
};
};
+/**
+ * Takes a URL query and transforms it into a "search" object
+ * @param {String?} query
+ * @returns {Object} A search object
+ */
export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true });
+ const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null;
return {
+ runnerType,
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
+ filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
@@ -50,8 +101,15 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
};
};
+/**
+ * Takes a "search" object and transforms it into a URL.
+ *
+ * @param {Object} search
+ * @param {String} url
+ * @returns {String} New URL for the page
+ */
export const fromSearchToUrl = (
- { filters = [], sort = null, pagination = {} },
+ { runnerType = null, filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const filterParams = {
@@ -65,6 +123,10 @@ export const fromSearchToUrl = (
}),
};
+ if (runnerType) {
+ filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType];
+ }
+
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
@@ -82,21 +144,31 @@ export const fromSearchToUrl = (
return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true);
};
-export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => {
+/**
+ * Takes a "search" object and transforms it into variables for runner a GraphQL query.
+ *
+ * @param {Object} search
+ * @returns {Object} Hash of filter values
+ */
+export const fromSearchToVariables = ({
+ runnerType = null,
+ filters = [],
+ sort = null,
+ pagination = {},
+} = {}) => {
const variables = {};
const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
});
- variables.search = queryObj[PARAM_KEY_SEARCH];
-
- // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
- [variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
-
+ variables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG];
+ if (runnerType) {
+ variables.type = runnerType;
+ }
if (sort) {
variables.sort = sort;
}
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 99cf16c8350..5c7cbeac5b2 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -13,9 +13,9 @@ export default {
ConfidentialityFilter,
},
computed: {
- ...mapState(['query']),
+ ...mapState(['urlQuery', 'sidebarDirty']),
showReset() {
- return this.query.state || this.query.confidential;
+ return this.urlQuery.state || this.urlQuery.confidential;
},
},
methods: {
@@ -32,7 +32,7 @@ export default {
<status-filter />
<confidentiality-filter />
<div class="gl-display-flex gl-align-items-center gl-mt-3">
- <gl-button category="primary" variant="confirm" size="small" type="submit">
+ <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
<gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index 73911b9d319..aa7c26b8044 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -1,7 +1,7 @@
<script>
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, __ } from '~/locale';
export default {
name: 'RadioFilter',
@@ -49,7 +49,7 @@ export default {
...mapActions(['setQuery']),
radioLabel(filter) {
return filter.value === this.ANY.value
- ? sprintf(s__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
+ ? sprintf(__('Any %{header}'), { header: this.filterData.header.toLowerCase() })
: filter.label;
},
},
diff --git a/app/assets/javascripts/search/sidebar/constants/state_filter_data.js b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
index 7c9a029ffe4..2f9f8a7cb46 100644
--- a/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
+++ b/app/assets/javascripts/search/sidebar/constants/state_filter_data.js
@@ -5,7 +5,7 @@ const header = __('Status');
const filters = {
ANY: {
label: __('Any'),
- value: 'all',
+ value: null,
},
OPEN: {
label: __('Open'),
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index be64a9278e3..a6af5644681 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -2,9 +2,9 @@ import Api from '~/api';
import createFlash from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
import * as types from './mutation_types';
-import { loadDataFromLS, setFrequentItemToLS, mergeById } from './utils';
+import { loadDataFromLS, setFrequentItemToLS, mergeById, isSidebarDirty } from './utils';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
@@ -86,8 +86,12 @@ export const setFrequentProject = ({ state, commit }, item) => {
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
};
-export const setQuery = ({ commit }, { key, value }) => {
+export const setQuery = ({ state, commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
+
+ if (SIDEBAR_PARAMS.includes(key)) {
+ commit(types.SET_SIDEBAR_DIRTY, isSidebarDirty(state.query, state.urlQuery));
+ }
};
export const applyQuery = ({ state }) => {
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index 3abf7cac6ba..678bd82c7a6 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -1,3 +1,6 @@
+import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
+import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
+
export const MAX_FREQUENT_ITEMS = 5;
export const MAX_FREQUENCY = 5;
@@ -5,3 +8,5 @@ export const MAX_FREQUENCY = 5;
export const GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects';
+
+export const SIDEBAR_PARAMS = [stateFilterData.filterParam, confidentialFilterData.filterParam];
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
index 5c1c29dc738..bf1e3e79cba 100644
--- a/app/assets/javascripts/search/store/mutation_types.js
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -7,5 +7,6 @@ export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY';
+export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY';
export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 63156a89738..5d154fe3aa0 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -26,6 +26,9 @@ export default {
[types.SET_QUERY](state, { key, value }) {
state.query[key] = value;
},
+ [types.SET_SIDEBAR_DIRTY](state, value) {
+ state.sidebarDirty = value;
+ },
[types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
state.frequentItems[key] = data;
},
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 5b1429ccc97..d4005697f35 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,6 +1,8 @@
+import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
const createState = ({ query }) => ({
+ urlQuery: cloneDeep(query),
query,
groups: [],
fetchingGroups: false,
@@ -10,5 +12,6 @@ const createState = ({ query }) => ({
[GROUPS_LOCAL_STORAGE_KEY]: [],
[PROJECTS_LOCAL_STORAGE_KEY]: [],
},
+ sidebarDirty: false,
});
export default createState;
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index b00b9bb0f2e..6b56ff0b5e5 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -1,5 +1,5 @@
import AccessorUtilities from '../../lib/utils/accessor';
-import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY } from './constants';
+import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY, SIDEBAR_PARAMS } from './constants';
function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]]));
@@ -80,3 +80,13 @@ export const mergeById = (inflatedData, storedData) => {
return { ...stored, ...data };
});
};
+
+export const isSidebarDirty = (currentQuery, urlQuery) => {
+ return SIDEBAR_PARAMS.some((param) => {
+ // userAddParam ensures we don't get a false dirty from null !== undefined
+ const userAddedParam = !urlQuery[param] && currentQuery[param];
+ const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param];
+
+ return userAddedParam || userChangedExistingParam;
+ });
+};
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 6c70a8c33db..bc13150c99c 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
@@ -31,6 +31,7 @@ export default {
AutoDevOpsAlert,
AutoDevOpsEnabledAlert,
FeatureCard,
+ GlAlert,
GlLink,
GlSprintf,
GlTab,
@@ -79,6 +80,7 @@ export default {
data() {
return {
autoDevopsEnabledAlertDismissedProjects: [],
+ errorMessage: '',
};
},
computed: {
@@ -106,6 +108,12 @@ export default {
dismissedProjects.add(this.projectPath);
this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
},
+ onError(message) {
+ this.errorMessage = message;
+ },
+ dismissAlert() {
+ this.errorMessage = '';
+ },
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
};
@@ -113,6 +121,16 @@ export default {
<template>
<article>
+ <gl-alert
+ v-if="errorMessage"
+ sticky
+ class="gl-top-8 gl-z-index-1"
+ data-testid="manage-via-mr-error-alert"
+ variant="danger"
+ @dismiss="dismissAlert"
+ >
+ {{ errorMessage }}
+ </gl-alert>
<local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects"
:storage-key="$options.autoDevopsEnabledAlertStorageKey"
@@ -174,6 +192,7 @@ export default {
data-testid="security-testing-card"
:feature="feature"
class="gl-mb-6"
+ @error="onError"
/>
</template>
</section-layout>
@@ -207,6 +226,7 @@ export default {
:key="feature.type"
:feature="feature"
class="gl-mb-6"
+ @error="onError"
/>
</template>
</section-layout>
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 6a282df99bf..9c80506549e 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -3,6 +3,7 @@ import { __, s__ } from '~/locale';
import {
REPORT_TYPE_SAST,
+ REPORT_TYPE_SAST_IAC,
REPORT_TYPE_DAST,
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_SECRET_DETECTION,
@@ -16,6 +17,7 @@ import {
} from '~/vue_shared/security_reports/constants';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
+import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql';
import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
/**
@@ -30,6 +32,19 @@ export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sas
anchor: 'configuration',
});
+export const SAST_IAC_NAME = __('Infrastructure as Code (IaC) Scanning');
+export const SAST_IAC_SHORT_NAME = s__('ciReport|IaC Scanning');
+export const SAST_IAC_DESCRIPTION = __(
+ 'Analyze your infrastructure as code configuration files for known vulnerabilities.',
+);
+export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index');
+export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/iac_scanning/index',
+ {
+ anchor: 'configuration',
+ },
+);
+
export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
export const DAST_SHORT_NAME = s__('ciReport|DAST');
export const DAST_DESCRIPTION = __('Analyze a review version of your web application.');
@@ -141,6 +156,27 @@ export const securityFeatures = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
canEnableByMergeRequest: true,
},
+ ...(gon?.features?.configureIacScanningViaMr
+ ? [
+ {
+ name: SAST_IAC_NAME,
+ shortName: SAST_IAC_SHORT_NAME,
+ description: SAST_IAC_DESCRIPTION,
+ helpPath: SAST_IAC_HELP_PATH,
+ configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST_IAC,
+
+ // This field is currently hardcoded because SAST IaC is always available.
+ // It will eventually come from the Backend, the progress is tracked in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
+ available: true,
+
+ // This field will eventually come from the backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: true,
+ },
+ ]
+ : []),
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
@@ -242,6 +278,21 @@ export const featureToMutationMap = {
},
}),
},
+ ...(gon?.features?.configureIacScanningViaMr
+ ? {
+ [REPORT_TYPE_SAST_IAC]: {
+ mutationId: 'configureSastIac',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastIacMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ }),
+ },
+ }
+ : {}),
[REPORT_TYPE_SECRET_DETECTION]: {
mutationId: 'configureSecretDetection',
getMutationPayload: (projectPath) => ({
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 86afdbfeb8c..33d72b54f86 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -66,6 +66,11 @@ export default {
return Boolean(name && description && configurationText);
},
},
+ methods: {
+ onError(message) {
+ this.$emit('error', message);
+ },
+ },
i18n: {
enabled: s__('SecurityConfiguration|Enabled'),
notEnabled: s__('SecurityConfiguration|Not enabled'),
@@ -129,6 +134,7 @@ export default {
category="primary"
class="gl-mt-5"
:data-qa-selector="`${feature.type}_mr_button`"
+ @error="onError"
/>
<gl-button
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql
new file mode 100644
index 00000000000..26b826ef722
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_iac.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSastIac($input: ConfigureSastIacInput!) {
+ configureSastIac(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 60d2c0d4e5a..a8623b468f2 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -14,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
const {
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 b1c8f6ef22e..0021fe909e5 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
@@ -339,7 +339,7 @@ export default {
</div>
<div class="gl-display-flex">
<span class="gl-text-gray-600 gl-ml-5">
- {{ s__('SetStatusModal|A busy indicator is shown next to your name and avatar.') }}
+ {{ s__('SetStatusModal|An indicator appears next to your name and avatar') }}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index e41bb41dc05..bdd014163a0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -39,6 +39,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -58,7 +61,12 @@ export default {
</template>
</span>
- <uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
+ <uncollapsed-assignee-list
+ v-else
+ :users="sortedAssigness"
+ :issuable-type="issuableType"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index 80caebad39d..a3379784bc1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -32,6 +32,11 @@ export default {
return this.users.length === 0;
},
},
+ methods: {
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
+ },
};
</script>
@@ -61,6 +66,7 @@ export default {
:users="users"
:issuable-type="issuableType"
class="gl-text-gray-800 gl-mt-2 hide-collapsed"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index c6877226b7d..453dd1b0580 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -125,6 +125,9 @@ export default {
availability: this.assigneeAvailabilityStatus[username] || '',
}));
},
+ toggleAttentionRequested(data) {
+ this.mediator.toggleAttentionRequested('assignee', data);
+ },
},
};
</script>
@@ -152,6 +155,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@assign-self="assignSelf"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index c2ca87af9ce..8d5c3b2def3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,6 +1,8 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
+import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
import UserNameWithStatus from './user_name_with_status.vue';
@@ -8,9 +10,11 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
+ AttentionRequestedToggle,
AssigneeAvatarLink,
UserNameWithStatus,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@@ -32,6 +36,10 @@ export default {
return this.users[0];
},
hasOneUser() {
+ if (this.showVerticalList) {
+ return false;
+ }
+
return this.users.length === 1;
},
hiddenAssigneesLabel() {
@@ -45,6 +53,10 @@ export default {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
+ if (this.showVerticalList) {
+ return this.users;
+ }
+
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
@@ -53,6 +65,12 @@ export default {
username() {
return `@${this.firstUser.username}`;
},
+ showVerticalList() {
+ return this.glFeatures.mrAttentionRequests && this.isMergeRequest;
+ },
+ isMergeRequest() {
+ return this.issuableType === IssuableType.MergeRequest;
+ },
},
methods: {
toggleShowLess() {
@@ -64,6 +82,9 @@ export default {
}
return u?.status?.availability || '';
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -84,11 +105,34 @@ export default {
<div v-else>
<div class="gl-display-flex gl-flex-wrap">
<div
- v-for="user in uncollapsedUsers"
+ v-for="(user, index) in uncollapsedUsers"
:key="user.id"
- class="user-item gl-display-inline-block"
+ :class="{
+ 'user-item': !showVerticalList,
+ 'gl-mb-3': index !== users.length - 1 && showVerticalList,
+ }"
+ class="gl-display-inline-block"
>
- <assignee-avatar-link :user="user" :issuable-type="issuableType" />
+ <attention-requested-toggle
+ v-if="showVerticalList && user.can_update_merge_request"
+ :user="user"
+ type="assignee"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
+ <assignee-avatar-link
+ :user="user"
+ :issuable-type="issuableType"
+ :tooltip-has-name="!showVerticalList"
+ >
+ <div
+ v-if="showVerticalList"
+ class="gl-ml-3 gl-line-height-normal gl-display-grid"
+ data-testid="username"
+ >
+ <user-name-with-status :name="user.name" :availability="userAvailability(user)" />
+ <span>@{{ user.username }}</span>
+ </div>
+ </assignee-avatar-link>
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
new file mode 100644
index 00000000000..38ba468d197
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+
+export default {
+ i18n: {
+ attentionRequestedReviewer: __('Request attention to review'),
+ attentionRequestedAssignee: __('Request attention'),
+ removeAttentionRequested: __('Remove attention request'),
+ },
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ tooltipTitle() {
+ if (this.user.attention_requested) {
+ return this.$options.i18n.removeAttentionRequested;
+ }
+
+ return this.type === 'reviewer'
+ ? this.$options.i18n.attentionRequestedReviewer
+ : this.$options.i18n.attentionRequestedAssignee;
+ },
+ },
+ methods: {
+ toggleAttentionRequired() {
+ if (this.loading) return;
+
+ this.$root.$emit(BV_HIDE_TOOLTIP);
+ this.loading = true;
+ this.$emit('toggle-attention-requested', {
+ user: this.user,
+ callback: this.toggleAttentionRequiredComplete,
+ });
+ },
+ toggleAttentionRequiredComplete() {
+ this.loading = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <span v-gl-tooltip.left.viewport="tooltipTitle">
+ <gl-button
+ :loading="loading"
+ :variant="user.attention_requested ? 'warning' : 'default'"
+ :icon="user.attention_requested ? 'star' : 'star-o'"
+ :aria-label="tooltipTitle"
+ size="small"
+ category="tertiary"
+ @click="toggleAttentionRequired"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index d5647619ea3..5cd4a1a5192 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const mutationMap = {
@@ -48,6 +49,7 @@ export default {
return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
+ LabelType,
};
},
methods: {
@@ -154,13 +156,12 @@ export default {
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
:labels-filter-base-path="projectIssuesPath"
- :labels-select-in-progress="isLabelsSelectInProgress"
- :selected-labels="selectedLabels"
:variant="$options.variant"
:issuable-type="issuableType"
+ workspace-type="project"
+ :attr-workspace-path="fullPath"
+ :label-create-type="LabelType.project"
data-qa-selector="labels_block"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index 5729b958b5d..b07fd944ff9 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -49,6 +49,9 @@ export default {
requestReview(data) {
this.$emit('request-review', data);
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
};
</script>
@@ -70,6 +73,7 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index e414aaf719b..2ea63219e92 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -88,6 +88,9 @@ export default {
requestReview(data) {
this.mediator.requestReview(data);
},
+ toggleAttentionRequested(data) {
+ this.mediator.toggleAttentionRequested('reviewer', data);
+ },
},
};
</script>
@@ -106,6 +109,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@request-review="requestReview"
+ @toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index 2922008cfb2..adaf1b65f3f 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,6 +1,8 @@
<script>
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf, s__ } from '~/locale';
+import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const LOADING_STATE = 'loading';
@@ -14,10 +16,12 @@ export default {
GlButton,
GlIcon,
ReviewerAvatarLink,
+ AttentionRequestedToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@@ -76,6 +80,9 @@ export default {
this.loadingStates[userId] = null;
}
},
+ toggleAttentionRequested(data) {
+ this.$emit('toggle-attention-requested', data);
+ },
},
LOADING_STATE,
SUCCESS_STATE,
@@ -90,6 +97,12 @@ export default {
:class="{ 'gl-mb-3': index !== users.length - 1 }"
data-testid="reviewer"
>
+ <attention-requested-toggle
+ v-if="glFeatures.mrAttentionRequests && user.can_update_merge_request"
+ :user="user"
+ type="reviewer"
+ @toggle-attention-requested="toggleAttentionRequested"
+ />
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
<span>{{ user.name }}</span>
@@ -113,7 +126,9 @@ export default {
data-testid="re-request-success"
/>
<gl-button
- v-else-if="user.can_update_merge_request && user.reviewed"
+ v-else-if="
+ user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests
+ "
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 8f4d5406da8..0ba8c4f8907 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -370,6 +370,7 @@ export default {
:loading="loading"
class="gl-w-full"
toggle-class="gl-max-w-100"
+ block
@shown="setFocus"
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 22adbd79ef6..056b3e98a1c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -134,7 +134,7 @@ export default {
v-if="canUpdate && !initialLoading && canEdit"
category="tertiary"
size="small"
- class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
+ class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
data-testid="edit-button"
:data-track-action="tracking.event"
:data-track-label="tracking.label"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index d4a8abb81a8..5d4031ac68b 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import createFlash from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -12,7 +12,7 @@ const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
export default {
components: {
GlLoadingIcon,
- GlTable,
+ GlTableLite,
},
inject: ['issuableType'],
props: {
@@ -89,7 +89,7 @@ export default {
<template>
<div>
<div v-if="isLoading"><gl-loading-icon size="md" /></div>
- <gl-table v-else :items="report" :fields="$options.fields" foot-clone>
+ <gl-table-lite v-else :items="report" :fields="$options.fields" foot-clone>
<template #cell(spentAt)="{ item: { spentAt } }">
<div>{{ formatDate(spentAt) }}</div>
</template>
@@ -111,6 +111,6 @@ export default {
<div>{{ getSummary(summary, note) }}</div>
</template>
<template #foot(note)>&nbsp;</template>
- </gl-table>
+ </gl-table-lite>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index e593973da82..ac34a75ac5c 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,4 +1,5 @@
-import { IssuableType } from '~/issue_show/constants';
+import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
+import { IssuableType, WorkspaceType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
@@ -29,11 +30,14 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
+import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
@@ -109,14 +113,30 @@ export const referenceQueries = {
},
};
-export const labelsQueries = {
+export const workspaceLabelsQueries = {
+ [WorkspaceType.project]: {
+ query: projectLabelsQuery,
+ },
+ [WorkspaceType.group]: {
+ query: groupLabelsQuery,
+ },
+};
+
+export const issuableLabelsQueries = {
[IssuableType.Issue]: {
issuableQuery: issueLabelsQuery,
- workspaceQuery: projectLabelsQuery,
+ mutation: updateIssueLabelsMutation,
+ mutationName: 'updateIssue',
+ },
+ [IssuableType.MergeRequest]: {
+ issuableQuery: mergeRequestLabelsQuery,
+ mutation: updateMergeRequestLabelsMutation,
+ mutationName: 'mergeRequestSetLabels',
},
[IssuableType.Epic]: {
issuableQuery: epicLabelsQuery,
- workspaceQuery: groupLabelsQuery,
+ mutation: updateEpicLabelsMutation,
+ mutationName: 'updateEpic',
},
};
diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js
index 1a806a051b7..6a670db2d38 100644
--- a/app/assets/javascripts/sidebar/graphql.js
+++ b/app/assets/javascripts/sidebar/graphql.js
@@ -25,7 +25,6 @@ export const defaultClient = createDefaultClient(resolvers, {
cacheConfig: {
fragmentMatcher,
},
- assumeImmutableResults: true,
});
export const apolloProvider = new VueApollo({
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5a2f4ebb0..898be4a97ce 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -260,6 +260,10 @@ export function mountSidebarLabels() {
variant: DropdownVariant.Sidebar,
canUpdate: parseBoolean(el.dataset.canEdit),
isClassicSidebar: true,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
},
render: (createElement) => createElement(SidebarLabels),
});
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
index 2aff7da4605..dd85eb1631b 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
@@ -1,4 +1,4 @@
-query($fullPath: ID!, $iid: String!) {
+query sidebarDetails($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
issue(iid: $iid) {
iid
diff --git a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
new file mode 100644
index 00000000000..a9f4af6e1b9
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
@@ -0,0 +1,7 @@
+mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestToggleAttentionRequested(
+ input: { projectPath: $projectPath, iid: $iid, userId: $userId }
+ ) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
index 28a47735143..2c6f379744e 100644
--- a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
@@ -1,4 +1,4 @@
-mutation($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
+mutation updateIssueHealthStatus($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
updateIssue(input: { projectPath: $projectPath, iid: $iid, healthStatus: $healthStatus }) {
issuable: issue {
id
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
index 69927ddd205..2d58843140f 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql
@@ -1,4 +1,4 @@
-mutation updateEpic($input: UpdateEpicInput!) {
+mutation updateIssuableConfidential($input: UpdateEpicInput!) {
issuableSetConfidential: updateEpic(input: $input) {
issuable: epic {
id
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
index af43766aed5..4a3090f3836 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
@@ -1,4 +1,4 @@
-mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
+mutation boardEpicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
updateIssuableSubscription: epicSetSubscription(
input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
index 317b48c142d..2e6bc8c36ba 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
@@ -1,4 +1,4 @@
-mutation updateEpic($input: UpdateEpicInput!) {
+mutation updateEpicTitle($input: UpdateEpicInput!) {
updateIssuableTitle: updateEpic(input: $input) {
epic {
title
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
index 81891fb601f..e1a3927e7e1 100644
--- a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
@@ -1,4 +1,4 @@
-mutation issueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+mutation projectIssueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
updateIssuableSubscription: issueSetSubscription(
input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
) {
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
index 3c09daad793..016c31ea096 100644
--- a/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_labels.mutation.graphql
@@ -2,6 +2,7 @@ mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
mergeRequestSetLabels(input: $input) {
errors
mergeRequest {
+ id
labels {
nodes {
color
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index cea26acd101..d8ab8f1c65b 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -5,6 +5,7 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
+import toggleAttentionRequestedMutation from '../queries/toggle_attention_requested.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@@ -90,4 +91,15 @@ export default class SidebarService {
},
});
}
+
+ toggleAttentionRequested(userId) {
+ return gqClient.mutate({
+ mutation: toggleAttentionRequestedMutation,
+ variables: {
+ userId: convertToGraphQLId(TYPE_USER, `${userId}`),
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 9144e3b08db..86580744ccc 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,6 +1,6 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
@@ -56,13 +56,52 @@ export default class SidebarMediator {
return this.service
.requestReview(userId)
.then(() => {
- this.store.updateReviewer(userId);
+ this.store.updateReviewer(userId, 'reviewed');
toast(__('Requested review'));
callback(userId, true);
})
.catch(() => callback(userId, false));
}
+ async toggleAttentionRequested(type, { user, callback }) {
+ try {
+ const isReviewer = type === 'reviewer';
+ const reviewerOrAssignee = isReviewer
+ ? this.store.findReviewer(user)
+ : this.store.findAssignee(user);
+
+ await this.service.toggleAttentionRequested(user.id);
+
+ if (reviewerOrAssignee.attention_requested) {
+ toast(
+ sprintf(__('Removed attention request from @%{username}'), {
+ username: user.username,
+ }),
+ );
+ } else {
+ toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
+ }
+
+ this.store.updateReviewer(user.id, 'attention_requested');
+ this.store.updateAssignee(user.id, 'attention_requested');
+
+ callback();
+ } catch (error) {
+ callback();
+ createFlash({
+ message: sprintf(__('Updating the attention request for %{username} failed.'), {
+ username: user.username,
+ }),
+ error,
+ captureError: true,
+ actionConfig: {
+ title: __('Try again'),
+ clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
+ },
+ });
+ }
+ }
+
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 94c54fc0980..5376791469e 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -82,11 +82,19 @@ export default class SidebarStore {
}
}
- updateReviewer(id) {
+ updateAssignee(id, stateKey) {
+ const assignee = this.findAssignee({ id });
+
+ if (assignee) {
+ assignee[stateKey] = !assignee[stateKey];
+ }
+ }
+
+ updateReviewer(id, stateKey) {
const reviewer = this.findReviewer({ id });
if (reviewer) {
- reviewer.reviewed = false;
+ reviewer[stateKey] = !reviewer[stateKey];
}
}
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index 8e7368ef804..21f38c4d8c9 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -18,7 +18,6 @@ export default function appFactory(el, Component) {
{},
{
batchMax: 1,
- assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
index 2ae2baddbcc..53572e680e5 100644
--- a/app/assets/javascripts/static_site_editor/graphql/index.js
+++ b/app/assets/javascripts/static_site_editor/graphql/index.js
@@ -22,7 +22,6 @@ const createApolloProvider = (appData) => {
},
{
typeDefs,
- assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 70e692a0c86..eb03aa3cca3 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -30,7 +30,7 @@ export default {
updatedFileDescription() {
const { sourcePath } = this.appData;
- return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
+ return sprintf(__('Update %{sourcePath} file'), { sourcePath });
},
},
created() {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 93353b400e5..79a30340856 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -12,6 +12,7 @@ export default class TaskList {
this.lockVersion = options.lockVersion;
this.taskListContainerSelector = `${this.selector} .js-task-list-container`;
this.updateHandler = this.update.bind(this);
+ this.onUpdate = options.onUpdate || (() => {});
this.onSuccess = options.onSuccess || (() => {});
this.onError =
options.onError ||
@@ -96,6 +97,7 @@ export default class TaskList {
},
};
+ this.onUpdate();
this.disableTaskListItems(e);
return axios
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
new file mode 100644
index 00000000000..aedf5b6acfe
--- /dev/null
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -0,0 +1,117 @@
+<script>
+import $ from 'jquery';
+import { GlButton, GlIntersectionObserver, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+
+import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+import '~/behaviors/markdown/render_gfm';
+
+export default {
+ name: 'TermsApp',
+ i18n: {
+ accept: __('Accept terms'),
+ continue: __('Continue'),
+ decline: __('Decline and sign out'),
+ },
+ flashElements: [],
+ csrf,
+ directives: {
+ SafeHtml,
+ },
+ components: { GlButton, GlIntersectionObserver },
+ inject: ['terms', 'permissions', 'paths'],
+ data() {
+ return {
+ acceptDisabled: true,
+ };
+ },
+ computed: {
+ isLoggedIn,
+ },
+ mounted() {
+ this.renderGFM();
+ this.setScrollableViewportHeight();
+
+ this.$options.flashElements = [
+ ...document.querySelectorAll(
+ Object.values(FLASH_TYPES)
+ .map((flashType) => `.flash-${flashType}`)
+ .join(','),
+ ),
+ ];
+
+ this.$options.flashElements.forEach((flashElement) => {
+ flashElement.addEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ });
+ },
+ beforeDestroy() {
+ this.$options.flashElements.forEach((flashElement) => {
+ flashElement.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ });
+ },
+ methods: {
+ renderGFM() {
+ $(this.$refs.gfmContainer).renderGFM();
+ },
+ handleBottomReached() {
+ this.acceptDisabled = false;
+ },
+ setScrollableViewportHeight() {
+ // Reset `max-height` inline style
+ this.$refs.scrollableViewport.style.maxHeight = '';
+
+ const { scrollHeight, clientHeight } = document.documentElement;
+
+ // Set `max-height` to 100vh minus all elements that are NOT the scrollable viewport (header, footer, alerts, etc)
+ this.$refs.scrollableViewport.style.maxHeight = `calc(100vh - ${
+ scrollHeight - clientHeight
+ }px)`;
+ },
+ handleFlashClose(event) {
+ this.setScrollableViewportHeight();
+ event.target.removeEventListener(FLASH_CLOSED_EVENT, this.handleFlashClose);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-card-body gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
+ <div
+ class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none"
+ ></div>
+ <div
+ ref="scrollableViewport"
+ data-testid="scrollable-viewport"
+ class="gl-h-100vh gl-overflow-y-auto gl-pb-11 gl-px-5"
+ >
+ <div ref="gfmContainer" v-safe-html="terms"></div>
+ <gl-intersection-observer @appear="handleBottomReached">
+ <div></div>
+ </gl-intersection-observer>
+ </div>
+ </div>
+ <div v-if="isLoggedIn" class="gl-card-footer gl-display-flex gl-justify-content-end">
+ <form v-if="permissions.canDecline" method="post" :action="paths.decline">
+ <gl-button type="submit">{{ $options.i18n.decline }}</gl-button>
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+ <form v-if="permissions.canAccept" class="gl-ml-3" method="post" :action="paths.accept">
+ <gl-button
+ type="submit"
+ variant="confirm"
+ :disabled="acceptDisabled"
+ data-qa-selector="accept_terms_button"
+ >{{ $options.i18n.accept }}</gl-button
+ >
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+ <gl-button v-else class="gl-ml-3" :href="paths.root" variant="confirm">{{
+ $options.i18n.continue
+ }}</gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/terms/index.js b/app/assets/javascripts/terms/index.js
new file mode 100644
index 00000000000..9d60fdfb50a
--- /dev/null
+++ b/app/assets/javascripts/terms/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+
+import TermsApp from 'jh_else_ce/terms/components/app.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export const initTermsApp = () => {
+ const el = document.getElementById('js-terms-of-service');
+
+ if (!el) return false;
+
+ const { terms, permissions, paths } = convertObjectPropsToCamelCase(
+ JSON.parse(el.dataset.termsData),
+ { deep: true },
+ );
+
+ return new Vue({
+ el,
+ provide: { terms, permissions, paths },
+ render(createElement) {
+ return createElement(TermsApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 8d29a65d705..6a29883290a 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -6,7 +6,7 @@ import TokenAccess from './components/token_access.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
diff --git a/app/assets/javascripts/user_lists/components/user_lists_table.vue b/app/assets/javascripts/user_lists/components/user_lists_table.vue
index 765f59228a6..ccc2bfabb56 100644
--- a/app/assets/javascripts/user_lists/components/user_lists_table.vue
+++ b/app/assets/javascripts/user_lists/components/user_lists_table.vue
@@ -23,7 +23,7 @@ export default {
translations: {
createdTimeagoLabel: s__('UserList|created %{timeago}'),
deleteListTitle: s__('UserList|Delete %{name}?'),
- deleteListMessage: s__('User list %{name} will be removed. Are you sure?'),
+ deleteListMessage: __('User list %{name} will be removed. Are you sure?'),
editUserListLabel: s__('FeatureFlags|Edit User List'),
},
modal: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
new file mode 100644
index 00000000000..492e68b636f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { escape } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { n__, s__ } from '~/locale';
+
+const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
+
+export default {
+ components: {
+ GlSprintf,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ isSquashEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFastForwardEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ commitsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ targetBranchEscaped() {
+ return escape(this.targetBranch);
+ },
+ commitsCountMessage() {
+ return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
+ },
+ message() {
+ return this.isFastForwardEnabled
+ ? s__('mrWidgetCommitsAdded|Adds %{commitCount} to %{targetBranch}.')
+ : s__(
+ 'mrWidgetCommitsAdded|Adds %{commitCount} and %{mergeCommitCount} to %{targetBranch}%{squashedCommits}.',
+ );
+ },
+ textDecorativeComponent() {
+ return this.glFeatures.restructuredMrWidget ? 'span' : 'strong';
+ },
+ },
+ mergeCommitCount,
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="message">
+ <template #commitCount>
+ <component :is="textDecorativeComponent" class="commits-count-message">{{
+ commitsCountMessage
+ }}</component>
+ </template>
+ <template #mergeCommitCount>
+ <component :is="textDecorativeComponent">{{ $options.mergeCommitCount }}</component>
+ </template>
+ <template #targetBranch>
+ <span class="label-branch">{{ targetBranchEscaped }}</span>
+ </template>
+ <template #squashedCommits>
+ <template v-if="glFeatures.restructuredMrWidget && isSquashEnabled">
+ {{ n__('(squashes %d commit)', '(squashes %d commits)', commitsCount) }}</template
+ ></template
+ >
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 0c4a5ee35d9..25dbb614c1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -1,7 +1,11 @@
<script>
import { toNounSeriesText } from '~/lib/utils/grammar';
import { n__, sprintf } from '~/locale';
-import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
+import {
+ APPROVED_BY_YOU_AND_OTHERS,
+ APPROVED_BY_YOU,
+ APPROVED_BY_OTHERS,
+} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
export default {
@@ -29,12 +33,23 @@ export default {
},
},
computed: {
- message() {
- if (this.approved) {
- return APPROVED_MESSAGE;
+ approvalLeftMessage() {
+ if (this.rulesLeft.length) {
+ return sprintf(
+ n__(
+ 'Requires %{count} approval from %{names}.',
+ 'Requires %{count} approvals from %{names}.',
+ this.approvalsLeft,
+ ),
+ {
+ names: toNounSeriesText(this.rulesLeft),
+ count: this.approvalsLeft,
+ },
+ false,
+ );
}
- if (!this.rulesLeft.length) {
+ if (!this.approved) {
return n__(
'Requires %d approval from eligible users.',
'Requires %d approvals from eligible users.',
@@ -42,32 +57,51 @@ export default {
);
}
- return sprintf(
- n__(
- 'Requires %{count} approval from %{names}.',
- 'Requires %{count} approvals from %{names}.',
- this.approvalsLeft,
- ),
- {
- names: toNounSeriesText(this.rulesLeft),
- count: this.approvalsLeft,
- },
- false,
- );
+ return '';
+ },
+ message() {
+ if (this.approvedByMe && this.approvedByOthers) {
+ return APPROVED_BY_YOU_AND_OTHERS;
+ }
+
+ if (this.approvedByMe) {
+ return APPROVED_BY_YOU;
+ }
+
+ if (this.approved) {
+ return APPROVED_BY_OTHERS;
+ }
+
+ return '';
},
hasApprovers() {
return Boolean(this.approvers.length);
},
+ approvedByMe() {
+ if (!this.currentUserId) {
+ return false;
+ }
+ return this.approvers.some((approver) => approver.id === this.currentUserId);
+ },
+ approvedByOthers() {
+ if (!this.currentUserId) {
+ return false;
+ }
+ return this.approvers.some((approver) => approver.id !== this.currentUserId);
+ },
+ currentUserId() {
+ return gon.current_user_id;
+ },
},
- APPROVED_MESSAGE,
};
</script>
<template>
<div data-qa-selector="approvals_summary_content">
- <strong>{{ message }}</strong>
+ <strong>{{ approvalLeftMessage }}</strong>
<template v-if="hasApprovers">
- <span>{{ s__('mrWidget|Approved by') }}</span>
+ <span v-if="approvalLeftMessage">{{ message }}</span>
+ <strong v-else>{{ message }}</strong>
<user-avatar-list class="d-inline-block align-middle" :items="approvers" />
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
index 0538c38307b..fbdefa95630 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
@@ -6,4 +6,6 @@ export const FETCH_ERROR = s__(
);
export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.');
export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.');
-export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.');
+export const APPROVED_BY_YOU_AND_OTHERS = s__('mrWidget|Approved by you and others');
+export const APPROVED_BY_YOU = s__('mrWidget|Approved by you');
+export const APPROVED_BY_OTHERS = s__('mrWidget|Approved by');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index cbace1ad57c..f4f611dfd1b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -12,13 +12,12 @@ import {
CANCELED,
SKIPPED,
} from './constants';
-import MemoryUsage from './memory_usage.vue';
export default {
name: 'DeploymentInfo',
components: {
GlLink,
- MemoryUsage,
+ MemoryUsage: () => import('./memory_usage.vue'),
TooltipOnTruncate,
},
directives: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
index d3384903cce..655acf28253 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
@@ -2,10 +2,11 @@
import { GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import MrCollapsibleExtension from '../mr_collapsible_extension.vue';
+import Deployment from './deployment.vue';
export default {
components: {
- Deployment: () => import('./deployment.vue'),
+ Deployment,
GlSprintf,
MrCollapsibleExtension,
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
index 023367a794e..33a83aef057 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
@@ -24,13 +24,20 @@ export default {
return sprintf(__('%{widget} options'), { widget: this.widget });
},
},
+ methods: {
+ onClickAction(action) {
+ if (action.onClick) {
+ action.onClick();
+ }
+ },
+ },
};
</script>
<template>
<div>
<gl-dropdown
- v-if="tertiaryButtons"
+ v-if="tertiaryButtons.length"
:text="dropdownLabel"
icon="ellipsis_v"
no-caret
@@ -47,6 +54,7 @@ export default {
:key="index"
:href="btn.href"
:target="btn.target"
+ @click="onClickAction(btn)"
>
{{ btn.text }}
</gl-dropdown-item>
@@ -57,11 +65,12 @@ export default {
:key="index"
:href="btn.href"
:target="btn.target"
- :class="{ 'gl-mr-3': index > 1 }"
+ :class="{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }"
category="tertiary"
variant="confirm"
size="small"
- class="gl-display-none gl-md-display-block"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
>
{{ btn.text }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 298f7c7ad8c..6f10f788952 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -8,6 +8,8 @@ import {
GlTooltipDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
+import { once } from 'lodash';
+import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import { EXTENSION_ICON_CLASS } from '../../constants';
@@ -102,8 +104,15 @@ export default {
});
},
methods: {
+ triggerRedisTracking: once(function triggerRedisTracking() {
+ if (this.$options.expandEvent) {
+ api.trackRedisHllUserEvent(this.$options.expandEvent);
+ }
+ }),
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
+
+ this.triggerRedisTracking();
},
loadAllData() {
if (this.fullData) return;
@@ -143,7 +152,10 @@ export default {
:is-loading="isLoadingSummary"
:icon-name="statusIconName"
/>
- <div class="media-body gl-display-flex gl-flex-direction-row!">
+ <div
+ class="media-body gl-display-flex gl-flex-direction-row!"
+ data-testid="widget-extension-top-level"
+ >
<div class="gl-flex-grow-1">
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<div v-else v-safe-html="summary(collapsedData)"></div>
@@ -194,20 +206,28 @@ export default {
class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7"
data-testid="extension-list-item"
>
- <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" />
+ <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
<gl-intersection-observer
:options="{ rootMargin: '100px', thresholds: 0.1 }"
- class="gl-flex-wrap gl-align-self-center gl-display-flex"
+ class="gl-flex-wrap gl-display-flex gl-w-full"
@appear="appear(index)"
@disappear="disappear(index)"
>
- <div v-safe-html="data.text" class="gl-mr-4"></div>
+ <div
+ v-safe-html="data.text"
+ class="gl-mr-4 gl-display-flex gl-align-items-center"
+ ></div>
<div v-if="data.link">
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
</div>
<gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
{{ data.badge.text }}
</gl-badge>
+ <actions
+ :widget="$options.label || $options.name"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto"
+ />
</gl-intersection-observer>
</li>
</smart-virtual-list>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 4ca0b660696..ec6e6ed2620 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -12,6 +12,7 @@ export const registerExtension = (extension) => {
name: extension.name,
props: extension.props,
i18n: extension.i18n,
+ expandEvent: extension.expandEvent,
computed: {
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
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 5c67b9c7ab5..9070cb1fe65 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
@@ -151,7 +151,7 @@ export default {
right
data-qa-selector="download_dropdown"
>
- <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header>
+ <gl-dropdown-section-header>{{ __('Download as') }}</gl-dropdown-section-header>
<gl-dropdown-item
:href="mr.emailPatchesPath"
class="js-download-email-patches"
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 9bb955c534f..f7c952f9ef6 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
@@ -101,6 +101,9 @@ export default {
? this.pipeline.details.status
: {};
},
+ artifacts() {
+ return this.pipeline?.details?.artifacts;
+ },
hasStages() {
return this.pipeline?.details?.stages?.length > 0;
},
@@ -285,7 +288,7 @@ export default {
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
- <pipeline-artifacts :pipeline-id="pipeline.id" class="gl-ml-3" />
+ <pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 306026072a3..c314261d3f5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,8 +1,10 @@
<script>
import { s__, n__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'MRWidgetRelatedLinks',
+ mixins: [glFeatureFlagMixin()],
props: {
relatedLinks: {
type: Object,
@@ -14,6 +16,11 @@ export default {
required: false,
default: '',
},
+ showAssignToMe: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
closesText() {
@@ -30,16 +37,25 @@ export default {
};
</script>
<template>
- <section class="mr-info-list gl-ml-7 gl-pb-5">
- <p v-if="relatedLinks.closing">
+ <section>
+ <p
+ v-if="relatedLinks.closing"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
{{ closesText }}
<span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
</p>
- <p v-if="relatedLinks.mentioned">
+ <p
+ v-if="relatedLinks.mentioned"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
</p>
- <p v-if="relatedLinks.assignToMe">
+ <p
+ v-if="relatedLinks.assignToMe && showAssignToMe"
+ :class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
+ >
<span v-html="relatedLinks.assignToMe /* eslint-disable-line vue/no-v-html */"></span>
</p>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index f3673005c45..cd5b7c3110d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -4,9 +4,7 @@ import Tracking from '~/tracking';
import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
import {
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_CONTENT,
SP_HELP_URL,
@@ -20,9 +18,7 @@ export default {
name: 'MRWidgetSuggestPipeline',
SP_ICON_NAME,
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_CONTENT,
SP_HELP_URL,
@@ -81,29 +77,14 @@ export default {
<div>
<gl-sprintf
:message="
- s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
- %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd}
- to create one.`)
+ s__(`mrWidget|%{boldHeaderStart}Looks like there's no pipeline here.%{boldHeaderEnd}`)
"
>
- <template #prefixToLink="{ content }">
+ <template #boldHeader="{ content }">
<strong>
{{ content }}
</strong>
</template>
- <template #addPipelineLink="{ content }">
- <gl-link
- :href="pipelinePath"
- class="gl-ml-1"
- data-testid="add-pipeline-link"
- :data-track-property="humanAccess"
- :data-track-value="$options.SP_LINK_TRACK_VALUE"
- :data-track-action="$options.SP_LINK_TRACK_EVENT"
- :data-track-label="$options.SP_TRACK_LABEL"
- >
- {{ content }}
- </gl-link>
- </template>
</gl-sprintf>
</div>
</template>
@@ -115,9 +96,6 @@ export default {
</div>
<div class="col-md-7 order-md-first col-12">
<div class="ml-6 gl-pt-5">
- <strong>
- {{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
- </strong>
<p class="gl-mt-2">
<gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
@@ -142,7 +120,7 @@ export default {
:data-track-action="$options.SP_SHOW_TRACK_EVENT"
:data-track-label="$options.SP_TRACK_LABEL"
>
- {{ __('Show me how to add a pipeline') }}
+ {{ __('Try out GitLab Pipelines') }}
</gl-button>
</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 9268e426954..caafd6b995e 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
@@ -4,7 +4,7 @@ import { __ } from '../../locale';
export default {
i18n: {
- removesBranchText: __('The source branch will be deleted'),
+ removesBranchText: __('Deletes the source branch'),
tooltipTitle: __('A user with write access to the source branch selected this option'),
},
components: {
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
index 44bdc4a3be8..3eda2828e97 100644
--- 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
@@ -1,5 +1,8 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ mixins: [glFeatureFlagMixin()],
props: {
value: {
type: String,
@@ -20,7 +23,10 @@ export default {
<template>
<li>
<div class="commit-message-editor">
- <div class="d-flex flex-wrap align-items-center justify-content-between">
+ <div
+ :class="{ 'gl-mb-3': glFeatures.restructuredMrWidget }"
+ class="d-flex flex-wrap align-items-center justify-content-between"
+ >
<label class="col-form-label" :for="inputId">
<strong>{{ label }}</strong>
</label>
@@ -35,7 +41,7 @@ export default {
rows="7"
@input="$emit('input', $event.target.value)"
></textarea>
- <slot name="checkbox"></slot>
+ <slot name="text-muted"></slot>
</div>
</li>
</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
index 3ca193514f1..5c4a526bcc3 100644
--- 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
@@ -1,15 +1,12 @@
<script>
-import { GlButton, GlSprintf } from '@gitlab/ui';
-import { escape } from 'lodash';
-import { __, n__, s__ } from '~/locale';
-
-const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import AddedCommitMessage from '../added_commit_message.vue';
export default {
- mergeCommitCount,
components: {
GlButton,
- GlSprintf,
+ AddedCommitMessage,
},
props: {
isSquashEnabled: {
@@ -39,9 +36,6 @@ export default {
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
- commitsCountMessage() {
- return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
- },
modifyLinkMessage() {
if (this.isFastForwardEnabled) return __('Modify commit message');
else if (this.isSquashEnabled) return __('Modify commit messages');
@@ -50,16 +44,6 @@ export default {
ariaLabel() {
return this.expanded ? __('Collapse') : __('Expand');
},
- targetBranchEscaped() {
- return escape(this.targetBranch);
- },
- message() {
- return this.isFastForwardEnabled
- ? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
- : s__(
- 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
- );
- },
},
methods: {
toggle() {
@@ -86,17 +70,12 @@ export default {
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span class="vertical-align-middle">
- <gl-sprintf :message="message">
- <template #commitCount>
- <strong class="commits-count-message">{{ commitsCountMessage }}</strong>
- </template>
- <template #mergeCommitCount>
- <strong>{{ $options.mergeCommitCount }}</strong>
- </template>
- <template #targetBranch>
- <span class="label-branch">{{ targetBranchEscaped }}</span>
- </template>
- </gl-sprintf>
+ <added-commit-message
+ :is-squash-enabled="isSquashEnabled"
+ :is-fast-forward-enabled="isFastForwardEnabled"
+ :commits-count="commitsCount"
+ :target-branch="targetBranch"
+ />
</span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 0eb173edbcb..a44caf886a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -177,10 +177,10 @@ export default {
</h4>
<section class="mr-info-list">
<p v-if="shouldRemoveSourceBranch">
- {{ s__('mrWidget|The source branch will be deleted') }}
+ {{ s__('mrWidget|Deletes the source branch') }}
</p>
<p v-else class="gl-display-flex">
- <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
+ <span class="gl-mr-3">{{ s__('mrWidget|Does not delete the source branch') }}</span>
<gl-button
v-if="canRemoveSourceBranch"
:loading="isRemovingSourceBranch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index e02be6dc2f7..10b93d7849f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -6,11 +7,12 @@ export default {
components: {
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
- <status-icon :show-disabled-button="true" status="loading" />
+ <status-icon :show-disabled-button="!glFeatures.restructuredMrWidget" status="loading" />
<div class="media-body space-children">
<span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span>
</div>
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 a1759b1a815..84dac95ce74 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
@@ -1,6 +1,7 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import api from '~/api';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
@@ -83,6 +84,8 @@ export default {
removeSourceBranch() {
this.isMakingRequest = true;
+ api.trackRedisHllUserEvent('i_code_review_post_merge_delete_branch');
+
this.service
.removeSourceBranch()
.then((res) => res.data)
@@ -103,9 +106,13 @@ export default {
});
},
openRevertModal() {
+ api.trackRedisHllUserEvent('i_code_review_post_merge_click_revert');
+
modalEventHub.$emit(OPEN_REVERT_MODAL);
},
openCherryPickModal() {
+ api.trackRedisHllUserEvent('i_code_review_post_merge_click_cherry_pick');
+
modalEventHub.$emit(OPEN_CHERRY_PICK_MODAL);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
index 1c245b584ea..247877a8235 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -32,7 +32,7 @@ export default {
</h4>
<section class="mr-info-list">
<p>
- {{ s__('mrWidget|The changes will be merged into') }}
+ {{ s__('mrWidget|Merges changes into') }}
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 1976d3639a6..9f2870d8d69 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,8 +1,7 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
-import { escape } from 'lodash';
import createFlash from '~/flash';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
@@ -85,13 +84,7 @@ export default {
return ['failed', 'loading'].includes(this.status);
},
fastForwardMergeText() {
- return sprintf(
- __('Merge blocked: the source branch must be rebased onto the target branch.'),
- {
- targetBranch: `<span class="label-branch">${escape(this.targetBranch)}</span>`,
- },
- false,
- );
+ return __('Merge blocked: the source branch must be rebased onto the target branch.');
},
},
methods: {
@@ -170,8 +163,8 @@ export default {
v-if="!rebaseInProgress && !canPushToSourceBranch"
class="gl-font-weight-bold gl-ml-0!"
data-testid="rebase-message"
- v-html="fastForwardMergeText /* eslint-disable-line vue/no-v-html */"
- ></span>
+ >{{ fastForwardMergeText }}</span
+ >
<div
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
index 9a7743348ff..0b6aa104181 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
@@ -37,7 +37,7 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
- <p class="media-body gl-m-0! gl-font-weight-bold">
+ <p class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!">
<template v-if="canMerge">
{{ __('Ready to merge!') }}
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 7827c79cd31..2d704d3b07a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
+import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
@@ -22,6 +23,11 @@ export default {
data() {
return { emptyStateSVG };
},
+ methods: {
+ onClickNewFile() {
+ api.trackRedisHllUserEvent('i_code_review_widget_nothing_merge_click_new_file');
+ },
+ },
ciHelpPage: helpPagePath('/ci/quick_start/index.html'),
safeHtmlConfig: { ADD_TAGS: ['use'] },
};
@@ -59,6 +65,7 @@ export default {
category="secondary"
variant="success"
data-testid="createFileButton"
+ @click="onClickNewFile"
>
{{ __('Create file') }}
</gl-button>
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 7d4bd4cf1bf..d2cc99302a9 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
@@ -18,9 +18,10 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests
import createFlash from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { helpPagePath } from '~/helpers/help_page_helper';
import MergeRequest from '../../../merge_request';
import {
AUTO_MERGE_STRATEGIES,
@@ -35,6 +36,8 @@ import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
+import AddedCommitMessage from '../added_commit_message.vue';
+import RelatedLinks from '../mr_widget_related_links.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
import CommitsHeader from './commits_header.vue';
@@ -113,6 +116,8 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
+ AddedCommitMessage,
+ RelatedLinks,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -134,6 +139,7 @@ export default {
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
isPipelineFailedModalVisible: false,
+ editCommitMessage: false,
};
},
computed: {
@@ -162,7 +168,7 @@ export default {
},
isMergeAllowed() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.mergeable || false;
+ return this.state.mergeable;
}
return this.mr.isMergeAllowed;
@@ -174,6 +180,11 @@ export default {
return this.mr.canRemoveSourceBranch;
},
+ commitTemplateHelpPage() {
+ return helpPagePath('user/project/merge_requests/commit_templates.md', {
+ anchor: 'merge-commit-message-template',
+ });
+ },
commits() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.commitsWithoutMergeCommits.nodes;
@@ -279,6 +290,10 @@ export default {
return enableSquashBeforeMerge && this.commitsCount > 1;
},
shouldShowMergeControls() {
+ if (this.glFeatures.restructuredMrWidget) {
+ return this.restructuredWidgetShowMergeButtons;
+ }
+
return this.isMergeAllowed || this.isAutoMergeAvailable;
},
shouldShowSquashEdit() {
@@ -297,15 +312,26 @@ export default {
showDangerMessageForMergeTrain() {
return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed;
},
+ restructuredWidgetShowMergeButtons() {
+ if (this.glFeatures.restructuredMrWidget) {
+ return this.isMergeAllowed && this.state.userPermissions.canMerge;
+ }
+
+ return true;
+ },
},
mounted() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
}
},
beforeDestroy() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$off('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$off('mr.discussion.updated', this.updateGraphqlState);
}
if (this.pollingInterval) {
@@ -327,15 +353,6 @@ export default {
updateGraphqlState() {
return this.$apollo.queries.state.refetch();
},
- updateMergeCommitMessage(includeDescription) {
- const commitMessage = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.defaultMergeCommitMessage
- : this.mr.commitMessage;
- const commitMessageWithDescription = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.defaultMergeCommitMessageWithDescription
- : this.mr.commitMessageWithDescription;
- this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
- },
handleMergeButtonClick(useAutoMerge, mergeImmediately = false, confirmationClicked = false) {
if (this.showFailedPipelineModal && !confirmationClicked) {
this.isPipelineFailedModalVisible = true;
@@ -488,11 +505,21 @@ export default {
});
},
},
+ i18n: {
+ mergeCommitTemplateHintText: s__(
+ 'mrWidget|To change this default message, edit the template for merge commit messages. %{linkStart}Learn more.%{linkEnd}',
+ ),
+ },
};
</script>
<template>
- <div>
+ <div
+ :class="{
+ 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7':
+ glFeatures.restructuredMrWidget,
+ }"
+ >
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
<gl-skeleton-loader :width="418" :height="30">
@@ -504,11 +531,16 @@ export default {
</div>
</div>
<template v-else>
- <div class="mr-widget-body media">
- <status-icon :status="iconClass" />
+ <div
+ class="mr-widget-body media"
+ :class="{
+ 'mr-widget-body-line-height-1': glFeatures.restructuredMrWidget,
+ }"
+ >
+ <status-icon v-if="!glFeatures.restructuredMrWidget" :status="iconClass" />
<div class="media-body">
- <div class="mr-widget-body-controls gl-display-flex gl-align-items-center">
- <gl-button-group class="gl-align-self-start">
+ <div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
+ <gl-button-group v-if="restructuredWidgetShowMergeButtons" class="gl-align-self-start">
<gl-button
size="medium"
category="primary"
@@ -555,14 +587,27 @@ export default {
</gl-button-group>
<div
v-if="shouldShowMergeControls"
+ :class="{ 'gl-w-full gl-order-n1 gl-mb-5': glFeatures.restructuredMrWidget }"
class="gl-display-flex gl-align-items-center gl-flex-wrap"
>
+ <merge-train-helper-icon
+ v-if="shouldRenderMergeTrainHelperIcon"
+ :merge-train-when-pipeline-succeeds-docs-path="
+ mr.mergeTrainWhenPipelineSucceedsDocsPath
+ "
+ class="gl-mx-3"
+ />
+
<gl-form-checkbox
v-if="canRemoveSourceBranch"
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox gl-mx-3 gl-display-flex gl-align-items-center"
+ :class="{
+ 'gl-mx-3': !glFeatures.restructuredMrWidget,
+ 'gl-mr-5': glFeatures.restructuredMrWidget,
+ }"
+ class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center"
>
{{ __('Delete source branch') }}
</gl-form-checkbox>
@@ -573,38 +618,146 @@ export default {
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
- class="gl-mx-3"
+ :class="{
+ 'gl-mx-3': !glFeatures.restructuredMrWidget,
+ 'gl-mr-5': glFeatures.restructuredMrWidget,
+ }"
/>
- <merge-train-helper-icon
- v-if="shouldRenderMergeTrainHelperIcon"
- :merge-train-when-pipeline-succeeds-docs-path="
- mr.mergeTrainWhenPipelineSucceedsDocsPath
+ <gl-form-checkbox
+ v-if="
+ glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)
"
- />
+ v-model="editCommitMessage"
+ class="gl-display-flex gl-align-items-center"
+ >
+ {{ __('Edit commit message') }}
+ </gl-form-checkbox>
+ </div>
+ <div
+ v-else-if="!glFeatures.restructuredMrWidget"
+ class="bold js-resolve-mr-widget-items-message gl-ml-3"
+ >
+ <div
+ v-if="hasPipelineMustSucceedConflict"
+ class="gl-display-flex gl-align-items-center"
+ data-testid="pipeline-succeed-conflict"
+ >
+ <gl-sprintf :message="pipelineMustSucceedConflictText" />
+ <gl-link
+ :href="mr.pipelineMustSucceedDocsPath"
+ target="_blank"
+ class="gl-display-flex gl-ml-2"
+ >
+ <gl-icon name="question" />
+ </gl-link>
+ </div>
+ <gl-sprintf v-else :message="mergeDisabledText" />
</div>
- <template v-else>
- <div class="bold js-resolve-mr-widget-items-message gl-ml-3">
- <div
- v-if="hasPipelineMustSucceedConflict"
- class="gl-display-flex gl-align-items-center"
- data-testid="pipeline-succeed-conflict"
+ <template v-if="glFeatures.restructuredMrWidget">
+ <div v-show="editCommitMessage" class="gl-w-full gl-order-n1">
+ <ul
+ :class="{
+ 'content-list': !glFeatures.restructuredMrWidget,
+ 'gl-list-style-none gl-p-0 gl-pt-4': glFeatures.restructuredMrWidget,
+ }"
+ class="border-top commits-list flex-list"
>
- <gl-sprintf :message="pipelineMustSucceedConflictText" />
- <gl-link
- :href="mr.pipelineMustSucceedDocsPath"
- target="_blank"
- class="gl-display-flex gl-ml-2"
+ <commit-edit
+ v-if="shouldShowSquashEdit"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ class="gl-m-0! gl-p-0!"
+ >
+ <template #header>
+ <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ </template>
+ </commit-edit>
+ <commit-edit
+ v-if="shouldShowMergeEdit"
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ class="gl-m-0! gl-p-0!"
>
- <gl-icon name="question" />
- </gl-link>
- </div>
- <gl-sprintf v-else :message="mergeDisabledText" />
+ <template #text-muted>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link
+ :href="commitTemplateHelpPage"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </commit-edit>
+ </ul>
+ </div>
+ <div
+ v-if="!restructuredWidgetShowMergeButtons"
+ class="gl-w-full gl-order-n1 gl-text-gray-500"
+ >
+ <strong>
+ {{ __('Merge details') }}
+ </strong>
+ <ul class="gl-pl-4 gl-m-0">
+ <li class="gl-line-height-normal">
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ </li>
+ <li class="gl-line-height-normal">
+ <template v-if="removeSourceBranch">
+ {{ __('Deletes the source branch.') }}
+ </template>
+ <template v-else>
+ {{ __('Does not delete the source branch.') }}
+ </template>
+ </li>
+ <li v-if="mr.relatedLinks" class="gl-line-height-normal">
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </li>
+ </ul>
+ </div>
+ <div
+ v-else
+ :class="{ 'gl-mb-5': restructuredWidgetShowMergeButtons }"
+ class="gl-w-full gl-order-n1 gl-text-gray-500"
+ >
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ <template v-if="mr.relatedLinks">
+ &middot;
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </template>
</div>
</template>
</div>
<div
- v-if="showDangerMessageForMergeTrain"
+ v-if="showDangerMessageForMergeTrain && !glFeatures.restructuredMrWidget"
class="gl-mt-5 gl-text-gray-500"
data-testid="failed-pipeline-merge-train-text"
>
@@ -612,7 +765,7 @@ export default {
</div>
</div>
</div>
- <template v-if="shouldShowMergeControls">
+ <template v-if="shouldShowMergeControls && !glFeatures.restructuredMrWidget">
<div
v-if="!shouldShowMergeEdit"
class="mr-fast-forward-message"
@@ -621,7 +774,7 @@ export default {
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
- v-if="shouldShowSquashEdit || shouldShowMergeEdit"
+ v-if="!glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)"
:is-squash-enabled="squashBeforeMerge"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
@@ -646,15 +799,16 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
- <template #checkbox>
- <label>
- <input
- id="include-description"
- type="checkbox"
- @change="updateMergeCommitMessage($event.target.checked)"
- />
- {{ __('Include merge request description') }}
- </label>
+ <template #text-muted>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.mergeCommitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</template>
</commit-edit>
</ul>
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 41b5983ae0c..c6227c4394d 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,15 +1,18 @@
<script>
-import { GlIcon, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlIcon,
GlFormCheckbox,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: {
...SQUASH_BEFORE_MERGE,
},
@@ -33,6 +36,9 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
+ helpIconName() {
+ return this.glFeatures.restructuredMrWidget ? 'question-o' : 'question';
+ },
},
};
</script>
@@ -51,18 +57,18 @@ export default {
>
{{ $options.i18n.checkboxLabel }}
</gl-form-checkbox>
- <a
+ <gl-link
v-if="helpPath"
v-gl-tooltip
:href="helpPath"
:title="$options.i18n.helpLabel"
+ :class="{ 'gl-text-blue-600': glFeatures.restructuredMrWidget }"
target="_blank"
- rel="noopener noreferrer nofollow"
>
- <gl-icon name="question" />
+ <gl-icon :name="helpIconName" />
<span class="sr-only">
{{ $options.i18n.helpLabel }}
</span>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 790870ee4c6..fa4f8b76cb9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
-import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
-import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
+import draftQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -23,7 +23,7 @@ export default {
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
- query: workInProgressQuery,
+ query: draftQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
@@ -53,25 +53,25 @@ export default {
},
},
methods: {
- removeWipMutation() {
+ removeDraftMutation() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
this.$apollo
.mutate({
- mutation: removeWipMutation,
+ mutation: removeDraftMutation,
variables: {
...mergeRequestQueryVariables,
- wip: false,
+ draft: false,
},
update(
store,
{
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
errors,
- mergeRequest: { mergeableDiscussionsState, workInProgress, title },
+ mergeRequest: { mergeableDiscussionsState, draft, title },
},
},
},
@@ -91,7 +91,7 @@ export default {
const data = produce(sourceData, (draftState) => {
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
- draftState.project.mergeRequest.workInProgress = workInProgress;
+ draftState.project.mergeRequest.draft = draft;
draftState.project.mergeRequest.title = title;
});
@@ -104,14 +104,14 @@ export default {
optimisticResponse: {
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload',
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
mergeableDiscussionsState: true,
title: this.mr.title,
- workInProgress: false,
+ draft: false,
},
},
},
@@ -119,7 +119,7 @@ export default {
.then(
({
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
mergeRequest: { title },
},
},
@@ -137,9 +137,9 @@ export default {
this.isMakingRequest = false;
});
},
- handleRemoveWIP() {
+ handleRemoveDraft() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.removeWipMutation();
+ this.removeDraftMutation();
} else {
this.isMakingRequest = true;
this.service
@@ -178,8 +178,8 @@ export default {
size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
- class="js-remove-wip gl-ml-3"
- @click="handleRemoveWIP"
+ class="js-remove-draft gl-ml-3"
+ @click="handleRemoveDraft"
>
{{ s__('mrWidget|Mark as ready') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index b88e83ccb0f..d0c6cf12e25 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -17,14 +17,12 @@ export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY,
// SP - "Suggest Pipelines"
export const SP_TRACK_LABEL = 'no_pipeline_noticed';
-export const SP_LINK_TRACK_EVENT = 'click_link';
export const SP_SHOW_TRACK_EVENT = 'click_button';
-export const SP_LINK_TRACK_VALUE = 30;
export const SP_SHOW_TRACK_VALUE = 10;
export const SP_HELP_CONTENT = s__(
- `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
+ `mrWidget|GitLab %{linkStart}CI/CD can automatically build, test, and deploy your application.%{linkEnd} It only takes a few minutes to get started, and we can help you create a pipeline configuration file.`,
);
-export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/';
+export const SP_HELP_URL = 'https://docs.gitlab.com/ee/ci/quick_start/';
export const SP_ICON_NAME = 'status_notfound';
export const MERGE_ACTIVE_STATUS_PHRASES = [
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 349e9d29355..9cbc0b0e5d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -11,6 +11,7 @@ export default {
label: 'Issues',
loading: 'Loading issues...',
},
+ expandEvent: 'i_testing_load_performance_widget_total',
// Add an array of props
// These then get mapped to values stored in the MR Widget store
props: ['targetProjectFullPath', 'conflictsDocsPath'],
@@ -29,7 +30,15 @@ export default {
// Tertiary action buttons that will take the user elsewhere
// in the GitLab app
tertiaryButtons() {
- return [{ text: 'Full report', href: this.conflictsDocsPath, target: '_blank' }];
+ return [
+ {
+ text: 'Click me',
+ onClick() {
+ console.log('Hello world');
+ },
+ },
+ { text: 'Full report', href: this.conflictsDocsPath, target: '_blank' },
+ ];
},
},
methods: {
@@ -66,6 +75,7 @@ export default {
// href: 'https://google.com', // Required: href for the link
// text: 'Link text', // Required: Text to be used inside the link
// },
+ actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
}));
});
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
index 389a81e0a61..da1cace4598 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql
@@ -1,4 +1,4 @@
-query getIssues($projectPath: ID!) {
+query getProjectIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
issues {
count
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index f5dbcec7dbe..8d596465970 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -13,12 +13,7 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- assumeImmutableResults: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
export default () => {
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index cf6472f2c8c..83789f10285 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -1,9 +1,13 @@
import { __ } from '~/locale';
export const MERGE_DISABLED_TEXT = __('You can only merge once the items above are resolved.');
+export const MERGE_DISABLED_SKIPPED_PIPELINE_TEXT = __(
+ "Merge blocked: pipeline must succeed. It's waiting for a manual job to continue.",
+);
export const PIPELINE_MUST_SUCCEED_CONFLICT_TEXT = __(
'A CI/CD pipeline must run and be successful before merge.',
);
+export const PIPELINE_SKIPPED_STATUS = 'SKIPPED';
export default {
computed: {
@@ -17,6 +21,10 @@ export default {
);
},
mergeDisabledText() {
+ if (this.pipeline?.status === PIPELINE_SKIPPED_STATUS) {
+ return MERGE_DISABLED_SKIPPED_PIPELINE_TEXT;
+ }
+
return MERGE_DISABLED_TEXT;
},
pipelineMustSucceedConflictText() {
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 3ac1e881658..c98dc426224 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
@@ -91,6 +91,7 @@ export default {
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
+ ReadyToMerge: ReadyToMergeState,
},
apollo: {
state: {
@@ -213,6 +214,9 @@ export default {
window.gon?.features?.refactorMrWidgetsExtensionsUser
);
},
+ isRestructuredMrWidgetEnabled() {
+ return window.gon?.features?.restructuredMrWidget;
+ },
},
watch: {
'mr.machineValue': {
@@ -547,12 +551,17 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
-
- <div class="mr-widget-info">
+ <ready-to-merge
+ v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"
+ :mr="mr"
+ :service="service"
+ />
+ <div v-else class="mr-widget-info">
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:state="mr.state"
:related-links="mr.relatedLinks"
+ class="mr-info-list gl-ml-7 gl-pb-5"
/>
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 871aa880b36..bfb1517be81 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- workInProgress
+ draft
}
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
index daf21e75b3b..e0215fbd969 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
@@ -1,6 +1,6 @@
#import "./auto_merge_enabled.fragment.graphql"
-query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
+query autoMergeEnabled($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
index 186c0e64561..e66ac01ab12 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
@@ -1,4 +1,4 @@
-query workInProgressQuery($projectPath: ID!, $iid: String!) {
+query workInProgress($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
shouldBeRebased
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
new file mode 100644
index 00000000000..0983c28448e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -0,0 +1,9 @@
+query mrUserPermission($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ userPermissions {
+ updateMergeRequest
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
index 3b34be73c15..21c3ffd8321 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
@@ -1,4 +1,4 @@
-query readyToMergeQuery($projectPath: ID!, $iid: String!) {
+query getReadyToMergeStatus($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
userPermissions {
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 367b9ad1cdf..b2a1be5c5a9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -18,7 +18,9 @@ fragment ReadyToMerge on Project {
commitCount
diffHeadSha
userPermissions {
+ canMerge
removeSourceBranch
+ updateMergeRequest
}
targetBranch
mergeError
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
index 78259e1f553..f713739f65a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql
@@ -1,6 +1,6 @@
#import "./ready_to_merge.fragment.graphql"
-query readyToMergeQuery($projectPath: ID!, $iid: String!) {
+query readyToMerge($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
...ReadyToMerge
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
deleted file mode 100644
index 73e205ebf2b..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query workInProgressQuery($projectPath: ID!, $iid: String!) {
- project(fullPath: $projectPath) {
- mergeRequest(iid: $iid) {
- userPermissions {
- updateMergeRequest
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
new file mode 100644
index 00000000000..200fb1b7ca5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
@@ -0,0 +1,10 @@
+mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) {
+ mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) {
+ mergeRequest {
+ mergeableDiscussionsState
+ title
+ draft
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
deleted file mode 100644
index cfaa198d516..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) {
- mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) {
- mergeRequest {
- mergeableDiscussionsState
- title
- workInProgress
- }
- errors
- }
-}
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 65d78fc283c..2ae4f4da2f3 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
@@ -1,14 +1,14 @@
import { stateKey } from './state_maps';
export default function deviseState() {
- if (this.hasMergeChecksFailed) {
+ if (!this.commitsCount) {
+ return stateKey.nothingToMerge;
+ } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
} else if (this.projectArchived) {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (!this.commitsCount) {
- return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
return stateKey.checking;
} else if (this.hasConflicts) {
@@ -17,8 +17,8 @@ export default function deviseState() {
return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
- } else if (this.workInProgress) {
- return stateKey.workInProgress;
+ } else if (this.draft) {
+ return stateKey.draft;
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
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 6628225cd46..10a2907c81a 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
@@ -164,7 +164,7 @@ export default class MergeRequestStore {
this.projectArchived = data.project_archived;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.shouldBeRebased = Boolean(data.should_be_rebased);
- this.workInProgress = data.work_in_progress;
+ this.draft = data.draft;
}
const currentUser = data.current_user;
@@ -207,7 +207,7 @@ export default class MergeRequestStore {
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
- this.workInProgress = mergeRequest.workInProgress;
+ this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state;
this.setState();
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 4cb23407a74..9dfeaee905c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -4,7 +4,7 @@ export const stateToComponentMap = {
merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
- workInProgress: 'mr-widget-wip',
+ draft: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed',
@@ -24,7 +24,7 @@ export const stateToComponentMap = {
export const statesToShowHelpWidget = [
'merging',
'conflicts',
- 'workInProgress',
+ 'draft',
'readyToMerge',
'checking',
'unresolvedDiscussions',
@@ -40,7 +40,7 @@ export const stateKey = {
nothingToMerge: 'nothingToMerge',
checking: 'checking',
conflicts: 'conflicts',
- workInProgress: 'workInProgress',
+ draft: 'draft',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index f8f1613879f..6b774b2a734 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import NoteHeader from '~/notes/components/note_header.vue';
export default {
@@ -7,6 +7,9 @@ export default {
NoteHeader,
GlIcon,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
note: {
type: Object,
@@ -39,7 +42,7 @@ export default {
<div class="note-header">
<note-header :author="noteAuthor" :created-at="note.createdAt" :note-id="note.id">
- <span v-html="note.bodyHtml /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="note.bodyHtml"></span>
</note-header>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
index da5f1a00e11..0c26fcc0ab2 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_details.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/alert_detail_item.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
-query alertDetails($fullPath: ID!, $alertId: String) {
+query alertDetailsAssignees($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
alertManagementAlerts(iid: $alertId) {
nodes {
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index fda405c0fa5..9f1da9ae173 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -38,7 +38,6 @@ export default (selector) => {
return defaultDataIdFromObject(object);
},
},
- assumeImmutableResults: true,
}),
});
diff --git a/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue b/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
deleted file mode 100644
index 16ca2df02c0..00000000000
--- a/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script>
-import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlAlert,
- GlLink,
- GlSprintf,
- },
- inject: ['hasManagedPrometheus'],
- i18n: {
- alertsDeprecationText: s__(
- 'Metrics|GitLab-managed Prometheus is deprecated and %{linkStart}scheduled for removal%{linkEnd}. Following this removal, your existing alerts will continue to function as part of the new cluster integration. However, you will no longer be able to add new alerts or edit existing alerts from the metrics dashboard.',
- ),
- },
- methods: {
- helpPagePath,
- },
-};
-</script>
-
-<template>
- <gl-alert
- v-if="hasManagedPrometheus"
- variant="warning"
- class="my-2"
- data-testid="alerts-deprecation-warning"
- >
- <gl-sprintf :message="$options.i18n.alertsDeprecationText">
- <template #link="{ content }">
- <gl-link
- :href="
- helpPagePath('operations/metrics/alerts.html', {
- anchor: 'managed-prometheus-instances',
- })
- "
- target="_blank"
- >
- <span>{{ content }}</span>
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 40044e518c3..2c74d56f617 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@@ -9,6 +9,9 @@ export default {
components: {
GlIcon,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
data() {
@@ -65,7 +68,7 @@ export default {
<div class="blob-content">
<pre
class="code highlight"
- ><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre>
+ ><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index f388a468fd2..5de71c35be9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -21,6 +21,7 @@ import CiIcon from './ci_icon.vue';
* - Job show view - header
* - MR widget
* - Terraform table
+ * - On-demand scans list
*/
export default {
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
new file mode 100644
index 00000000000..4c07cf44fed
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { CONFIRM_DANGER_MODAL_ID } from './constants';
+import ConfirmDangerModal from './confirm_danger_modal.vue';
+
+export default {
+ name: 'ConfirmDanger',
+ components: {
+ GlButton,
+ ConfirmDangerModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ phrase: {
+ type: String,
+ required: true,
+ },
+ buttonText: {
+ type: String,
+ required: true,
+ },
+ buttonTestid: {
+ type: String,
+ required: false,
+ default: 'confirm-danger-button',
+ },
+ },
+ modalId: CONFIRM_DANGER_MODAL_ID,
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal="$options.modalId"
+ class="gl-button"
+ variant="danger"
+ :disabled="disabled"
+ :data-testid="buttonTestid"
+ >{{ buttonText }}</gl-button
+ >
+ <confirm-danger-modal
+ :modal-id="$options.modalId"
+ :phrase="phrase"
+ @confirm="$emit('confirm')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
new file mode 100644
index 00000000000..18fa297da87
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -0,0 +1,28 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import ConfirmDanger from './confirm_danger.vue';
+
+export default {
+ component: ConfirmDanger,
+ title: 'vue_shared/components/modals/confirm_danger_modal',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ConfirmDanger },
+ props: Object.keys(argTypes),
+ template: '<confirm-danger v-bind="$props" />',
+ provide: {
+ confirmDangerMessage: 'You require more Vespene Gas',
+ },
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ phrase: 'You must construct additional pylons',
+ buttonText: 'Confirm button text',
+};
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ ...Default.args,
+ disabled: true,
+};
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
new file mode 100644
index 00000000000..30c96daf7e3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlAlert, GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import {
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_TITLE,
+ CONFIRM_DANGER_PHRASE_TEXT,
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_MODAL_ERROR,
+} from './constants';
+
+export default {
+ name: 'ConfirmDangerModal',
+ components: {
+ GlAlert,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlSprintf,
+ },
+ inject: {
+ confirmDangerMessage: {
+ default: '',
+ },
+ confirmButtonText: {
+ default: CONFIRM_DANGER_MODAL_BUTTON,
+ },
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ phrase: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { confirmationPhrase: '' };
+ },
+ computed: {
+ isValid() {
+ return Boolean(
+ this.confirmationPhrase.length && this.equalString(this.confirmationPhrase, this.phrase),
+ );
+ },
+ actionPrimary() {
+ return {
+ text: this.confirmButtonText,
+ attributes: [{ variant: 'danger', disabled: !this.isValid }],
+ };
+ },
+ },
+ methods: {
+ equalString(a, b) {
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
+ },
+ },
+ i18n: {
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_TITLE,
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_PHRASE_TEXT,
+ CONFIRM_DANGER_MODAL_ERROR,
+ },
+};
+</script>
+<template>
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :data-testid="modalId"
+ :title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
+ :action-primary="actionPrimary"
+ @primary="$emit('confirm')"
+ >
+ <gl-alert
+ v-if="confirmDangerMessage"
+ variant="danger"
+ data-testid="confirm-danger-message"
+ :dismissible="false"
+ class="gl-mb-4"
+ >
+ {{ confirmDangerMessage }}
+ </gl-alert>
+ <p data-testid="confirm-danger-warning">{{ $options.i18n.CONFIRM_DANGER_WARNING }}</p>
+ <p data-testid="confirm-danger-phrase">
+ <gl-sprintf :message="$options.i18n.CONFIRM_DANGER_PHRASE_TEXT">
+ <template #phrase_code>
+ <code>{{ phrase }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="confirmationPhrase"
+ class="form-control"
+ data-testid="confirm-danger-input"
+ type="text"
+ />
+ </gl-form-group>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
new file mode 100644
index 00000000000..fa44a9be411
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/constants.js
@@ -0,0 +1,12 @@
+import { __ } from '~/locale';
+
+export const CONFIRM_DANGER_MODAL_ID = 'confirm-danger-modal';
+export const CONFIRM_DANGER_MODAL_TITLE = __('Confirmation required');
+export const CONFIRM_DANGER_MODAL_ERROR = __('Confirmation required');
+export const CONFIRM_DANGER_MODAL_BUTTON = __('Confirm');
+export const CONFIRM_DANGER_WARNING = __(
+ 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
+);
+export const CONFIRM_DANGER_PHRASE_TEXT = __(
+ 'Please type %{phrase_code} to proceed or close this modal to cancel.',
+);
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index ea507017caa..9cf8638f3cb 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,5 +1,8 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import {
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { forEach, escape } from 'lodash';
@@ -13,6 +16,9 @@ export default {
components: {
GlSkeletonLoading,
},
+ directives: {
+ SafeHtml,
+ },
props: {
content: {
type: String,
@@ -103,6 +109,7 @@ export default {
}
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'use'] },
};
</script>
@@ -111,8 +118,8 @@ export default {
<gl-skeleton-loading v-if="isLoading" />
<div
v-else
+ v-safe-html:[$options.safeHtmlConfig]="previewContent"
class="md gl-ml-auto gl-mr-auto"
- v-html="previewContent /* eslint-disable-line vue/no-v-html */"
></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index 7859ef85dd8..153b0981813 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -45,7 +45,7 @@ export default {
default: false,
},
selected: {
- type: Object,
+ type: [Object, Array],
required: false,
default: () => {},
},
@@ -54,6 +54,11 @@ export default {
required: false,
default: '',
},
+ allowMultiselect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isSearchEmpty() {
@@ -66,8 +71,14 @@ export default {
methods: {
selectOption(option) {
this.$emit('set-option', option || null);
+ if (!this.allowMultiselect) {
+ this.$refs.dropdown.hide();
+ }
},
isSelected(option) {
+ if (Array.isArray(this.selected)) {
+ return this.selected.some((label) => label.title === option.title);
+ }
return (
this.selected &&
((option.name && this.selected.name === option.name) ||
@@ -78,7 +89,7 @@ export default {
this.$refs.dropdown.show();
},
setFocus() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
setSearchTerm(search) {
this.$emit('set-search', search);
@@ -108,56 +119,60 @@ export default {
@shown="setFocus"
>
<template #header>
- <gl-search-box-by-type
- ref="search"
- :value="searchTerm"
- :placeholder="searchText"
- class="js-dropdown-input-field"
- @input="setSearchTerm"
- />
+ <slot name="header">
+ <gl-search-box-by-type
+ ref="search"
+ :value="searchTerm"
+ :placeholder="searchText"
+ class="js-dropdown-input-field"
+ @input="setSearchTerm"
+ />
+ </slot>
</template>
- <gl-dropdown-form class="gl-relative gl-min-h-7">
- <gl-loading-icon
- v-if="isLoading"
- size="md"
- class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
- />
- <template v-else>
- <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <slot name="default">
+ <gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
+ <gl-loading-icon
+ v-if="isLoading"
+ size="md"
+ class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <gl-dropdown-item
+ v-for="option in presetOptions"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click.native.capture.stop="selectOption(option)"
+ >
+ <slot name="preset-item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
<gl-dropdown-item
- v-for="option in presetOptions"
+ v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
- @click="selectOption(option)"
+ :avatar-url="avatarUrl(option)"
+ :secondary-text="secondaryText(option)"
+ data-testid="unselected-option"
+ @click.native.capture.stop="selectOption(option)"
>
- <slot name="preset-item" :item="option">
+ <slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
- <gl-dropdown-divider />
+ <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
+ {{ $options.i18n.noMatchingResults }}
+ </gl-dropdown-item>
</template>
- <gl-dropdown-item
- v-for="option in options"
- :key="option.id"
- :is-checked="isSelected(option)"
- :is-check-centered="true"
- :is-check-item="true"
- :avatar-url="avatarUrl(option)"
- :secondary-text="secondaryText(option)"
- data-testid="unselected-option"
- @click="selectOption(option)"
- >
- <slot name="item" :item="option">
- {{ option.title }}
- </slot>
- </gl-dropdown-item>
- <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
- {{ $options.i18n.noMatchingResults }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown-form>
+ </gl-dropdown-form>
+ </slot>
<template #footer>
<slot name="footer"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 276fb35b51f..adf34f822ed 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -86,7 +86,7 @@ export default {
<template>
<span>
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
- <gl-icon v-else-if="isSymlink" name="symlink" :size="size" use-deprecated-sizes />
+ <gl-icon v-else-if="isSymlink" name="symlink" :size="size" />
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 1df65d0a666..d9290e86bca 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -53,6 +53,7 @@ export const TOKEN_TITLE_ASSIGNEE = __('Assignee');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_LABEL = __('Label');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_ITERATION = __('Iteration');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 9dc5c5db276..7c1828f2294 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -171,15 +171,17 @@ export default {
* This watcher listens for updates to `filterValue` on
* such instances. :(
*/
- filterValue(value) {
- const [firstVal] = value;
+ filterValue(newValue, oldValue) {
+ const [firstVal] = newValue;
if (
!this.initialRender &&
- value.length === 1 &&
+ newValue.length === 1 &&
firstVal.type === 'filtered-search-term' &&
!firstVal.value.data
) {
- this.$emit('onFilter', []);
+ const filtersCleared =
+ oldValue[0].type !== 'filtered-search-term' || oldValue[0].value.data !== '';
+ this.$emit('onFilter', [], filtersCleared);
}
// Set initial render flag to false
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index ae5d3965de1..b3b3d5c88c6 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
-
+import { compact } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -59,8 +59,10 @@ export default {
.then((res) => {
// We'd want to avoid doing this check but
// users.json and /groups/:id/members & /projects/:id/users
- // return response differently.
- this.authors = Array.isArray(res) ? res : res.data;
+ // return response differently
+
+ // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
+ this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
createFlash({
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
index c1d1bc7da91..aff93ebc9c0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -1,16 +1,21 @@
<script>
-import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { GlDropdownDivider, GlDropdownSectionHeader, GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT_ITERATIONS } from '../constants';
export default {
components: {
BaseToken,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlFilteredSearchSuggestion,
},
+ mixins: [glFeatureFlagMixin()],
props: {
active: {
type: Boolean,
@@ -40,6 +45,27 @@ export default {
getActiveIteration(iterations, data) {
return iterations.find((iteration) => this.getValue(iteration) === data);
},
+ groupIterationsByCadence(iterations) {
+ const cadences = [];
+ iterations.forEach((iteration) => {
+ if (!iteration.iterationCadence) {
+ return;
+ }
+ const { title } = iteration.iterationCadence;
+ const cadenceIteration = {
+ id: iteration.id,
+ title: iteration.title,
+ period: this.getIterationPeriod(iteration),
+ };
+ const cadence = cadences.find((cad) => cad.title === title);
+ if (cadence) {
+ cadence.iterations.push(cadenceIteration);
+ } else {
+ cadences.push({ title, iterations: [cadenceIteration] });
+ }
+ });
+ return cadences;
+ },
fetchIterations(searchTerm) {
this.loading = true;
this.config
@@ -57,6 +83,16 @@ export default {
getValue(iteration) {
return String(getIdFromGraphQLId(iteration.id));
},
+ /**
+ * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
+ * This method also exists as a utility function in ee/../iterations/utils.js
+ * Remove the duplication when iteration token is moved to EE.
+ */
+ getIterationPeriod({ startDate, dueDate }) {
+ const start = formatDate(startDate, 'mmm d, yyyy', true);
+ const due = formatDate(dueDate, 'mmm d, yyyy', true);
+ return `${start} - ${due}`;
+ },
},
};
</script>
@@ -77,13 +113,26 @@ export default {
{{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
<template #suggestions-list="{ suggestions }">
- <gl-filtered-search-suggestion
- v-for="iteration in suggestions"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- </gl-filtered-search-suggestion>
+ <template v-for="(cadence, index) in groupIterationsByCadence(suggestions)">
+ <gl-dropdown-divider v-if="index !== 0" :key="index" />
+ <gl-dropdown-section-header
+ :key="cadence.title"
+ class="gl-overflow-hidden"
+ :title="cadence.title"
+ >
+ {{ cadence.title }}
+ </gl-dropdown-section-header>
+ <gl-filtered-search-suggestion
+ v-for="iteration in cadence.iterations"
+ :key="iteration.id"
+ :value="getValue(iteration)"
+ >
+ {{ iteration.title }}
+ <div v-if="glFeatures.iterationCadences" class="gl-text-gray-400">
+ {{ iteration.period }}
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
</template>
</base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
new file mode 100644
index 00000000000..f353cc3a765
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY } from '../constants';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ releases: this.config.initialReleases || [],
+ loading: false,
+ };
+ },
+ computed: {
+ defaultReleases() {
+ return this.config.defaultReleases || DEFAULT_NONE_ANY;
+ },
+ },
+ methods: {
+ getActiveRelease(releases, data) {
+ return releases.find((release) => release.tag.toLowerCase() === data.toLowerCase());
+ },
+ fetchReleases(searchTerm) {
+ this.loading = true;
+ this.config
+ .fetchReleases(searchTerm)
+ .then((response) => {
+ this.releases = response;
+ })
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching releases.') });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <base-token
+ :active="active"
+ :config="config"
+ :value="value"
+ :default-suggestions="defaultReleases"
+ :suggestions="releases"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveRelease"
+ @fetch-suggestions="fetchReleases"
+ v-on="$listeners"
+ >
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.tag : inputValue }}
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion
+ v-for="release in suggestions"
+ :key="release.id"
+ :value="release.tag"
+ >
+ {{ release.tag }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</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 41613bb3307..6ace0bd88f8 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,10 +1,16 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlTooltip, GlSafeHtmlDirective } from '@gitlab/ui';
+import {
+ GlTooltipDirective,
+ GlButton,
+ GlSafeHtmlDirective,
+ GlAvatarLink,
+ GlAvatarLabeled,
+} from '@gitlab/ui';
+import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
-import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -17,10 +23,9 @@ export default {
components: {
CiIconBadge,
TimeagoTooltip,
- UserAvatarImage,
- GlLink,
GlButton,
- GlTooltip,
+ GlAvatarLink,
+ GlAvatarLabeled,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -94,6 +99,9 @@ export default {
return this.itemName;
},
+ userId() {
+ return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
+ },
},
methods: {
@@ -124,24 +132,32 @@ export default {
{{ __('by') }}
<template v-if="user">
- <gl-link
- v-gl-tooltip
- :href="userPath"
- :title="user.email"
- class="js-user-link commit-committer-link"
+ <gl-avatar-link
+ :data-user-id="userId"
+ :data-username="user.username"
+ :data-name="user.name"
+ :href="user.webUrl"
+ target="_blank"
+ class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center"
>
- <user-avatar-image :img-src="avatarUrl" :img-alt="userAvatarAltText" :size="24" />
- {{ user.name }}
- </gl-link>
- <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
- {{ message }}
- </gl-tooltip>
- <span
- v-if="statusTooltipHTML"
- :ref="$options.EMOJI_REF"
- v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
- :data-testid="message"
- ></span>
+ <gl-avatar-labeled
+ :size="24"
+ :src="avatarUrl"
+ :label="user.name"
+ class="gl-display-none gl-sm-display-inline-flex gl-mx-1"
+ />
+ <strong class="author gl-display-inline gl-sm-display-none!">@{{ user.username }}</strong>
+ <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]">
+ {{ message }}
+ </gl-tooltip>
+ <span
+ v-if="statusTooltipHTML"
+ :ref="$options.EMOJI_REF"
+ v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
+ class="gl-ml-2"
+ :data-testid="message"
+ ></span>
+ </gl-avatar-link>
</template>
</section>
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 f9ae59567b2..648e9c9462f 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
@@ -1,11 +1,11 @@
<script>
-import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ApplySuggestion from './apply_suggestion.vue';
export default {
- components: { GlIcon, GlButton, GlLoadingIcon, ApplySuggestion },
+ components: { GlBadge, GlIcon, GlButton, GlLoadingIcon, ApplySuggestion },
directives: { 'gl-tooltip': GlTooltipDirective },
props: {
batchSuggestionsCount: {
@@ -134,8 +134,14 @@ export default {
<gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
- <div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div>
- <div v-else-if="isApplying" class="d-flex align-items-center text-secondary">
+ <gl-badge v-if="isApplied" variant="success" data-qa-selector="applied_badge">
+ {{ __('Applied') }}
+ </gl-badge>
+ <div
+ v-else-if="isApplying"
+ class="d-flex align-items-center text-secondary"
+ data-qa-selector="applying_badge"
+ >
<gl-loading-icon size="sm" class="d-flex-center mr-2" />
<span>{{ applyingSuggestionsMessage }}</span>
</div>
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 755e6f1f224..8877cfa39fb 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -26,6 +26,7 @@ import {
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
@@ -61,6 +62,9 @@ export default {
data() {
return {
expanded: false,
+ lines: [],
+ showLines: false,
+ loadingDiff: false,
};
},
computed: {
@@ -94,10 +98,25 @@ export default {
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
+ async toggleDiff() {
+ this.showLines = !this.showLines;
+
+ if (!this.lines.length) {
+ this.loadingDiff = true;
+ const { data } = await axios.get(this.note.outdated_line_change_path);
+
+ this.lines = data.map((l) => ({
+ ...l,
+ rich_text: l.rich_text.replace(/^[+ -]/, ''),
+ }));
+ this.loadingDiff = false;
+ }
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
},
+ userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
@@ -112,15 +131,28 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-safe-html="actionTextHtml"></span>
- <template v-if="canSeeDescriptionVersion" #extra-controls>
+ <template
+ v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
+ #extra-controls
+ >
&middot;
<gl-button
+ v-if="canSeeDescriptionVersion"
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
+ <gl-button
+ v-if="note.outdated_line_change_path"
+ :icon="showLines ? 'chevron-up' : 'chevron-down'"
+ variant="link"
+ data-testid="outdated-lines-change-btn"
+ @click="toggleDiff"
+ >
+ {{ __('Compare changes') }}
+ </gl-button>
</template>
</note-header>
</div>
@@ -154,6 +186,37 @@ export default {
@click="deleteDescriptionVersion"
/>
</div>
+ <div
+ v-if="lines.length && showLines"
+ class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ >
+ <table
+ :class="$options.userColorSchemeClass"
+ class="code js-syntax-highlight"
+ data-testid="outdated-lines"
+ >
+ <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
+ <td
+ :class="line.type"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.old_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.new_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="line_content gl-display-table-cell!"
+ v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
+ ></td>
+ </tr>
+ </table>
+ </div>
+ <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
index 4b21ec0330a..d108d8d689d 100644
--- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -69,20 +69,23 @@ export default {
/>
<div class="gl-display-flex gl-flex-direction-column">
- <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
+ <h2 class="gl-font-size-h1 gl-mt-3 gl-mb-0" data-testid="title">
<slot name="title">{{ title }}</slot>
- </h1>
+ </h2>
<div
v-if="$slots['sub-header']"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<slot name="sub-header"></slot>
</div>
</div>
</div>
- <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
+ <div
+ v-if="metadataSlots.length > 0"
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
+ >
<template v-if="!metadataLoading">
<div
v-for="(row, metadataIndex) in metadataSlots"
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index facace0d809..34845e3d9e4 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -2,6 +2,8 @@ import { s__ } from '~/locale';
export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
+export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
+
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
docker: {
instructions: s__(
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index d55c93fd146..d5493aa5a66 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -16,8 +16,9 @@ import { isEmpty } from 'lodash';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
- PLATFORMS_WITHOUT_ARCHITECTURES,
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ REGISTRATION_TOKEN_PLACEHOLDER,
} from './constants';
import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql';
@@ -41,7 +42,13 @@ export default {
props: {
modalId: {
type: String,
- required: true,
+ required: false,
+ default: 'runner-instructions-modal',
+ },
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
},
},
apollo: {
@@ -117,8 +124,20 @@ export default {
runnerInstallationLink() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link;
},
+ registerInstructionsWithToken() {
+ const { registerInstructions } = this.instructions || {};
+
+ if (this.registrationToken) {
+ return registerInstructions.replace(REGISTRATION_TOKEN_PLACEHOLDER, this.registrationToken);
+ }
+
+ return registerInstructions;
+ },
},
methods: {
+ show() {
+ this.$refs.modal.show();
+ },
selectPlatform(platform) {
this.selectedPlatform = platform;
@@ -158,9 +177,11 @@ export default {
</script>
<template>
<gl-modal
+ ref="modal"
:modal-id="modalId"
:title="$options.i18n.installARunner"
:action-secondary="$options.closeButton"
+ v-bind="$attrs"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
@@ -243,11 +264,11 @@ export default {
<pre
class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
data-testid="register-command"
- >{{ instructions.registerInstructions }}</pre
+ >{{ registerInstructionsWithToken }}</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
- :text="instructions.registerInstructions"
+ :text="registerInstructionsWithToken"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
index e75fedbb1d7..e68f0f31c13 100644
--- a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -24,10 +24,13 @@ export default {
},
},
data() {
+ const forceOpen = !this.collapsible || this.defaultExpanded;
return {
// Non-collapsible sections should always be expanded.
// For collapsible sections, fall back to defaultExpanded.
- sectionExpanded: !this.collapsible || this.defaultExpanded,
+ sectionExpanded: forceOpen,
+ initialised: forceOpen,
+ animating: false,
};
},
computed: {
@@ -53,7 +56,12 @@ export default {
toggleSectionExpanded() {
this.sectionExpanded = !this.sectionExpanded;
+ if (!this.initialised) {
+ this.initialised = true;
+ }
+
if (this.sectionExpanded) {
+ this.animating = true;
this.$refs.settingsContent.focus();
}
},
@@ -68,7 +76,10 @@ export default {
</script>
<template>
- <section class="settings" :class="{ 'no-animate': !slideAnimated, expanded: sectionExpanded }">
+ <section
+ class="settings"
+ :class="{ 'no-animate': !slideAnimated, expanded: sectionExpanded, animating }"
+ >
<div class="settings-header">
<h4>
<span
@@ -103,12 +114,14 @@ export default {
</p>
</div>
<div
+ v-show="initialised"
:id="settingsContentId"
ref="settingsContent"
:aria-labelledby="settingsLabelId"
tabindex="-1"
role="region"
class="settings-content"
+ @animationend="animating = false"
>
<slot></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index 6511c8d8c31..460a10e08ed 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -40,7 +40,7 @@ export default {
</script>
<template>
- <div v-gl-tooltip.left.viewport :class="containerClass" :title="tooltipText" @click="click">
+ <div v-gl-tooltip.left.viewport="tooltipText" :class="containerClass" @click="click">
<gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index d80b66fd9be..399db978b60 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -36,6 +36,7 @@ export default {
<template>
<div
class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
+ data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
:style="directionStyle"
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
index 122250d1ce7..8a26c4a6618 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
@@ -43,12 +43,7 @@ export default {
</script>
<template>
- <div
- v-gl-tooltip.left.viewport
- :title="labelsList"
- class="sidebar-collapsed-icon"
- @click="handleClick"
- >
+ <div v-gl-tooltip.left.viewport="labelsList" class="sidebar-collapsed-icon" @click="handleClick">
<gl-icon name="labels" />
<span>{{ labels.length }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 0ea22eb7aea..9e64f03fe84 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,5 +1,4 @@
import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
@@ -67,9 +66,11 @@ export default {
}
if (isScopedLabel(candidateLabel)) {
- const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
const currentActiveScopedLabel = state.labels.find(
- ({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
+ ({ set, title }) =>
+ set &&
+ title !== candidateLabel.title &&
+ scopedLabelKey({ title }) === scopedLabelKey(candidateLabel),
);
if (currentActiveScopedLabel) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
index 389eb174c0e..cd671b4d8f5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
@@ -1,7 +1,13 @@
export const SCOPED_LABEL_DELIMITER = '::';
+export const DEBOUNCE_DROPDOWN_DELAY = 200;
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
Embedded: 'embedded',
};
+
+export const LabelType = {
+ group: 'group',
+ project: 'project',
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 3ee0baf8812..f7485de0342 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,20 +1,25 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
+ DropdownHeader,
+ DropdownFooter,
GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
},
- inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
labelsCreateTitle: {
type: String,
@@ -48,10 +53,6 @@ export default {
type: String,
required: true,
},
- issuableType: {
- type: String,
- required: true,
- },
isVisible: {
type: Boolean,
required: false,
@@ -61,10 +62,17 @@ export default {
type: String,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: true,
+ },
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelCreateType: {
+ type: String,
+ required: true,
},
},
data() {
@@ -72,6 +80,7 @@ export default {
showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels],
isDirty: false,
+ searchKey: '',
};
},
computed: {
@@ -113,15 +122,24 @@ export default {
if (newVal) {
this.$refs.dropdown.show();
this.isDirty = false;
+ this.localSelectedLabels = this.selectedLabels;
} else {
this.$refs.dropdown.hide();
this.setLabels();
}
},
selectedLabels(newVal) {
- this.localSelectedLabels = newVal;
+ if (!this.isDirty) {
+ this.localSelectedLabels = newVal;
+ }
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ beforeDestroy() {
+ this.debouncedSearchKeyUpdate.cancel();
+ },
methods: {
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
@@ -140,10 +158,20 @@ export default {
this.$emit('setLabels', this.localSelectedLabels);
},
handleDropdownHide() {
+ this.$emit('closeDropdown');
if (!isDropdownVariantSidebar(this.variant)) {
this.setLabels();
}
},
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ setFocus() {
+ this.$refs.header.focusInput();
+ },
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
},
};
</script>
@@ -153,62 +181,44 @@ export default {
ref="dropdown"
:text="buttonText"
class="gl-w-full gl-mt-2"
+ data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
+ @shown="setFocus"
>
<template #header>
- <div
+ <dropdown-header
v-if="!isStandalone"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-header"
- >
- <gl-button
- v-if="showDropdownContentsCreateView"
- :aria-label="__('Go back')"
- variant="link"
- size="small"
- class="js-btn-back dropdown-header-button gl-p-0"
- icon="arrow-left"
- data-testid="go-back-button"
- @click.stop="toggleDropdownContent"
- />
- <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- data-testid="close-button"
- @click="$emit('closeDropdown')"
- />
- </div>
+ ref="header"
+ v-model="searchKey"
+ :labels-create-title="labelsCreateTitle"
+ :labels-list-title="labelsListTitle"
+ :show-dropdown-contents-create-view="showDropdownContentsCreateView"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ @closeDropdown="$emit('closeDropdown')"
+ @input="debouncedSearchKeyUpdate"
+ />
</template>
<template #default>
<component
:is="dropdownContentsView"
v-model="localSelectedLabels"
- :selected-labels="selectedLabels"
+ :search-key="searchKey"
:allow-multiselect="allowMultiselect"
- :issuable-type="issuableType"
:full-path="fullPath"
+ :workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
- @hideCreateView="toggleDropdownContentsCreateView"
+ :label-create-type="labelCreateType"
+ @hideCreateView="toggleDropdownContent"
/>
</template>
<template #footer>
- <div v-if="showDropdownFooter" data-testid="dropdown-footer">
- <gl-dropdown-item
- v-if="allowLabelCreate"
- data-testid="create-label-button"
- @click.capture.native.stop="toggleDropdownContent"
- >
- {{ footerCreateLabelTitle }}
- </gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
- {{ footerManageLabelTitle }}
- </gl-dropdown-item>
- </div>
+ <dropdown-footer
+ v-if="showDropdownFooter"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ />
</template>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index a2ed08e6b28..da626a21b14 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -2,10 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
+import { LabelType } from './constants';
const errorMessage = __('Error creating label.');
@@ -20,18 +20,21 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- issuableType: {
+ fullPath: {
type: String,
required: true,
},
- fullPath: {
+ attrWorkspacePath: {
type: String,
required: true,
},
- attrWorkspacePath: {
+ labelCreateType: {
+ type: String,
+ required: true,
+ },
+ workspaceType: {
type: String,
- required: false,
- default: undefined,
+ required: true,
},
},
data() {
@@ -50,25 +53,13 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- if (this.issuableType === IssuableType.Epic) {
- return {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.fullPath,
- };
- }
+ const attributePath = this.labelCreateType === LabelType.group ? 'groupPath' : 'projectPath';
- return this.attrWorkspacePath !== undefined
- ? {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.attrWorkspacePath,
- }
- : {
- title: this.labelTitle,
- color: this.selectedColor,
- projectPath: this.fullPath,
- };
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ [attributePath]: this.attrWorkspacePath,
+ };
},
},
methods: {
@@ -82,8 +73,10 @@ export default {
this.selectedColor = this.getColorCode(color);
},
updateLabelsInCache(store, label) {
+ const { query } = workspaceLabelsQueries[this.workspaceType];
+
const sourceData = store.readQuery({
- query: labelsQueries[this.issuableType].workspaceQuery,
+ query,
variables: { fullPath: this.fullPath, searchTerm: '' },
});
@@ -95,7 +88,7 @@ export default {
});
store.writeQuery({
- query: labelsQueries[this.issuableType].workspaceQuery,
+ query,
variables: { fullPath: this.fullPath, searchTerm: '' },
data,
});
@@ -180,7 +173,7 @@ export default {
<gl-button
class="js-btn-cancel-create"
data-testid="cancel-button"
- @click="$emit('hideCreateView')"
+ @click.stop="$emit('hideCreateView')"
>
{{ __('Cancel') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index e6a25362ff0..e9a2d7747e2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,18 +1,10 @@
<script>
-import {
- GlDropdownForm,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
export default {
@@ -20,7 +12,6 @@ export default {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
- GlSearchBoxByType,
GlIntersectionObserver,
LabelItem,
},
@@ -28,18 +19,10 @@ export default {
prop: 'localSelectedLabels',
},
props: {
- selectedLabels: {
- type: Array,
- required: true,
- },
allowMultiselect: {
type: Boolean,
required: true,
},
- issuableType: {
- type: String,
- required: true,
- },
localSelectedLabels: {
type: Array,
required: true,
@@ -48,10 +31,17 @@ export default {
type: String,
required: true,
},
+ searchKey: {
+ type: String,
+ required: true,
+ },
+ workspaceType: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- searchKey: '',
labels: [],
isVisible: false,
};
@@ -59,7 +49,7 @@ export default {
apollo: {
labels: {
query() {
- return labelsQueries[this.issuableType].workspaceQuery;
+ return workspaceLabelsQueries[this.workspaceType].query;
},
variables() {
return {
@@ -71,12 +61,6 @@ export default {
return this.searchKey.length === 1 || !this.isVisible;
},
update: (data) => data.workspace?.labels?.nodes || [],
- async result() {
- if (this.$refs.searchInput) {
- await this.$nextTick;
- this.$refs.searchInput.focusInput();
- }
- },
error() {
createFlash({ message: __('Error fetching labels.') });
},
@@ -101,12 +85,6 @@ export default {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- beforeDestroy() {
- this.debouncedSearchKeyUpdate.cancel();
- },
methods: {
isLabelSelected(label) {
return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
@@ -137,13 +115,7 @@ export default {
({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
);
} else {
- labels = [
- ...this.localSelectedLabels,
- {
- ...label,
- id: getIdFromGraphQLId(label.id),
- },
- ];
+ labels = [...this.localSelectedLabels, label];
}
this.$emit('input', labels);
},
@@ -153,12 +125,8 @@ export default {
this.$emit('closeDropdown', this.localSelectedLabels);
}
},
- setSearchKey(value) {
- this.searchKey = value;
- },
onDropdownAppear() {
this.isVisible = true;
- this.$refs.searchInput.focusInput();
},
},
};
@@ -167,14 +135,6 @@ export default {
<template>
<gl-intersection-observer @appear="onDropdownAppear">
<gl-dropdown-form class="labels-select-contents-list js-labels-list">
- <gl-search-box-by-type
- ref="searchInput"
- :value="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- data-testid="dropdown-input-field"
- @input="debouncedSearchKeyUpdate"
- />
<div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
new file mode 100644
index 00000000000..e67e704ffb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ inject: ['allowLabelCreate', 'labelsManagePath'],
+ props: {
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-footer">
+ <gl-dropdown-item
+ v-if="allowLabelCreate"
+ data-testid="create-label-button"
+ @click.capture.native.stop="$emit('toggleDropdownContentsCreateView')"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
+ {{ footerManageLabelTitle }}
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
new file mode 100644
index 00000000000..10064b01648
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlSearchBoxByType,
+ },
+ model: {
+ prop: 'searchKey',
+ },
+ props: {
+ labelsCreateTitle: {
+ type: String,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ showDropdownContentsCreateView: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFetchInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchKey: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownTitle() {
+ return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
+ },
+ },
+ methods: {
+ focusInput() {
+ this.$refs.searchInput?.focusInput();
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-header">
+ <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <gl-button
+ v-if="showDropdownContentsCreateView"
+ :aria-label="__('Go back')"
+ variant="link"
+ size="small"
+ class="js-btn-back dropdown-header-button gl-p-0"
+ icon="arrow-left"
+ data-testid="go-back-button"
+ @click.stop="$emit('toggleDropdownContentsCreateView')"
+ />
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ data-testid="close-button"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ <gl-search-box-by-type
+ v-if="!showDropdownContentsCreateView"
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="$emit('input', $event)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index 71d3d87cce5..aed5bc303ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,7 +1,6 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { sortBy } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
@@ -47,7 +46,7 @@ export default {
return this.allowScopedLabels && isScopedLabel(label);
},
removeLabel(labelId) {
- this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
+ this.$emit('onLabelRemove', labelId);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
index eb478645a03..a9c791091fc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
@@ -1,12 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) {
labelCreate(
input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath }
) {
label {
- id
- color
- description
- title
+ ...Label
}
errors
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
index a2e8579486f..c130cc426dc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -1,13 +1,12 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
issuable: epic(iid: $iid) {
id
labels {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
new file mode 100644
index 00000000000..45fcb50732e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
@@ -0,0 +1,15 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+mutation updateEpicLabels($input: UpdateEpicInput!) {
+ updateEpic(input: $input) {
+ epic {
+ id
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
index acc9bcd2015..ce1a69f84c0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
@@ -1,11 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) {
- labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
+ id
+ labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
index 1c2fd3bb7c0..e471d279b24 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
@@ -1,13 +1,12 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
issuable: issue(iid: $iid) {
id
labels {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
new file mode 100644
index 00000000000..dd80e89c8a7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
@@ -0,0 +1,14 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+query mergeRequestLabels($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ issuable: mergeRequest(iid: $iid) {
+ id
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
index dc39220487d..a7c24620aad 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
@@ -1,11 +1,11 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) {
+ id
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
- id
- title
- color
- description
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 6bd43da2203..97a65c13933 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -1,9 +1,12 @@
<script>
+import { debounce } from 'lodash';
+import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { labelsQueries } from '~/sidebar/constants';
-import { DropdownVariant } from './constants';
+import { issuableLabelsQueries } from '~/sidebar/constants';
+import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -50,16 +53,6 @@ export default {
required: false,
default: DropdownVariant.Sidebar,
},
- selectedLabels: {
- type: Array,
- required: false,
- default: () => [],
- },
- labelsSelectInProgress: {
- type: Boolean,
- required: false,
- default: false,
- },
labelsFilterBasePath: {
type: String,
required: false,
@@ -95,36 +88,44 @@ export default {
required: false,
default: __('Manage group labels'),
},
- isEditing: {
- type: Boolean,
- required: false,
- default: false,
- },
issuableType: {
type: String,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: true,
+ },
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelCreateType: {
+ type: String,
+ required: true,
},
},
data() {
return {
contentIsOnViewport: true,
issuableLabels: [],
+ labelsSelectInProgress: false,
+ oldIid: null,
+ sidebarExpandedOnClick: false,
};
},
computed: {
isLoading() {
return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
},
+ issuableLabelIds() {
+ return this.issuableLabels.map((label) => label.id);
+ },
},
apollo: {
issuableLabels: {
query() {
- return labelsQueries[this.issuableType].issuableQuery;
+ return issuableLabelsQueries[this.issuableType].issuableQuery;
},
skip() {
return !isDropdownVariantSidebar(this.variant);
@@ -143,16 +144,140 @@ export default {
},
},
},
+ watch: {
+ iid(_, oldVal) {
+ this.oldIid = oldVal;
+ },
+ },
+ mounted() {
+ document.addEventListener('toggleSidebarRevealLabelsDropdown', this.handleCollapsedValueClick);
+ },
+ beforeDestroy() {
+ document.removeEventListener(
+ 'toggleSidebarRevealLabelsDropdown',
+ this.handleCollapsedValueClick,
+ );
+ },
methods: {
handleDropdownClose(labels) {
- this.$emit('updateSelectedLabels', labels);
+ if (this.iid !== '') {
+ this.updateSelectedLabels(this.getUpdateVariables(labels));
+ } else {
+ this.$emit('updateSelectedLabels', { labels });
+ }
+
this.collapseEditableItem();
},
collapseEditableItem() {
this.$refs.editable?.collapse();
+ if (this.sidebarExpandedOnClick) {
+ this.sidebarExpandedOnClick = false;
+ this.$emit('toggleCollapse');
+ }
},
handleCollapsedValueClick() {
+ this.sidebarExpandedOnClick = true;
this.$emit('toggleCollapse');
+ debounce(() => {
+ this.$refs.editable.toggle();
+ this.$refs.dropdownContents.showDropdown();
+ }, DEBOUNCE_DROPDOWN_DELAY)();
+ },
+ getUpdateVariables(labels) {
+ let labelIds = [];
+
+ labelIds = labels.map(({ id }) => id);
+ const currentIid = this.oldIid || this.iid;
+
+ const updateVariables = {
+ iid: currentIid,
+ projectPath: this.fullPath,
+ labelIds,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return updateVariables;
+ case IssuableType.MergeRequest:
+ return {
+ ...updateVariables,
+ operationMode: MutationOperationMode.Replace,
+ };
+ case IssuableType.Epic:
+ return {
+ iid: currentIid,
+ groupPath: this.fullPath,
+ addLabelIds: labelIds.map((id) => getIdFromGraphQLId(id)),
+ removeLabelIds: this.issuableLabelIds
+ .filter((id) => !labelIds.includes(id))
+ .map((id) => getIdFromGraphQLId(id)),
+ };
+ default:
+ return {};
+ }
+ },
+ updateSelectedLabels(inputVariables) {
+ this.labelsSelectInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: issuableLabelsQueries[this.issuableType].mutation,
+ variables: { input: inputVariables },
+ })
+ .then(({ data }) => {
+ const { mutationName } = issuableLabelsQueries[this.issuableType];
+
+ if (data[mutationName]?.errors?.length) {
+ throw new Error();
+ }
+
+ this.$emit('updateSelectedLabels', {
+ id: data[mutationName]?.[this.issuableType]?.id,
+ labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
+ });
+ })
+ .catch((error) =>
+ createFlash({
+ message: __('An error occurred while updating labels.'),
+ captureError: true,
+ error,
+ }),
+ )
+ .finally(() => {
+ this.labelsSelectInProgress = false;
+ });
+ },
+ getRemoveVariables(labelId) {
+ const removeVariables = {
+ iid: this.iid,
+ projectPath: this.fullPath,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return {
+ ...removeVariables,
+ removeLabelIds: [labelId],
+ };
+ case IssuableType.MergeRequest:
+ return {
+ ...removeVariables,
+ labelIds: [labelId],
+ operationMode: MutationOperationMode.Remove,
+ };
+ case IssuableType.Epic:
+ return {
+ iid: this.iid,
+ removeLabelIds: [getIdFromGraphQLId(labelId)],
+ groupPath: this.fullPath,
+ };
+ default:
+ return {};
+ }
+ },
+ handleLabelRemove(labelId) {
+ this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -180,6 +305,7 @@ export default {
:title="__('Labels')"
:loading="isLoading"
:can-edit="allowLabelEdit"
+ @open="oldIid = null"
>
<template #collapsed>
<dropdown-value
@@ -188,7 +314,7 @@ export default {
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
@@ -201,23 +327,25 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
class="gl-mb-2"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
<dropdown-contents
+ ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
- :issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
+ :workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -233,10 +361,12 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
- :issuable-type="issuableType"
:full-path="fullPath"
+ :workspace-type="workspaceType"
+ :attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
@setLabels="handleDropdownClose"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
index a2990d7171b..dffcc053fac 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
-query timeTrackingReport($id: IssueID!) {
+query issueTimeTrackingReport($id: IssueID!) {
issuable: issue(id: $id) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
index 753f1b345e3..ede9b75d765 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
-query timeTrackingReport($id: MergeRequestID!) {
+query mrTimeTrackingReport($id: MergeRequestID!) {
issuable: mergeRequest(id: $id) {
__typename
id
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
index 1eea660d527..a16dcb6d893 100644
--- a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
@@ -5,10 +5,10 @@ import { OBSTACLE_TYPES } from './constants';
const OBSTACLE_TEXT = {
[OBSTACLE_TYPES.oncallSchedules]: s__(
- 'OnCallSchedules|On-call schedule %{obstacle} in Project %{project}',
+ 'OnCallSchedules|On-call schedule %{obstacle} in project %{project}',
),
[OBSTACLE_TYPES.escalationPolicies]: s__(
- 'EscalationPolicies|Escalation policy %{obstacle} in Project %{project}',
+ 'EscalationPolicies|Escalation policy %{obstacle} in project %{project}',
),
};
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 33fac5ebdbb..9cb66f6e65f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -63,3 +63,6 @@ export const timeRanges = [
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>
timeRanges.find((tr) => tr.name === timeWindowName);
+
+export const AVATAR_SHAPE_OPTION_CIRCLE = 'circle';
+export const AVATAR_SHAPE_OPTION_RECT = 'rect';
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index 779b04dc2bd..fc0ff78e7b4 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
/**
* Validation messages will take priority based on the property order.
@@ -12,11 +12,11 @@ import { s__ } from '~/locale';
const defaultFeedbackMap = {
valueMissing: {
isInvalid: (el) => el.validity?.valueMissing,
- message: s__('Please fill out this field.'),
+ message: __('Please fill out this field.'),
},
urlTypeMismatch: {
isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch,
- message: s__('Please enter a valid URL format, ex: http://www.example.com/home'),
+ message: __('Please enter a valid URL format, ex: http://www.example.com/home'),
},
};
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index c1e8376d656..114f60c96ee 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -82,7 +82,7 @@ export default {
});
this.$root.$on('clicked::link', (e) => {
- window.location = e.target.href;
+ window.location = e.currentTarget.href;
});
},
diff --git a/app/assets/javascripts/vue_shared/security_configuration/provider.js b/app/assets/javascripts/vue_shared/security_configuration/provider.js
index fa23669b615..ef96b443da8 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/provider.js
+++ b/app/assets/javascripts/vue_shared/security_configuration/provider.js
@@ -5,5 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index 3a4453bc7ae..e0669b3ed27 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -26,6 +26,11 @@ export default {
type: Number,
required: true,
},
+ injectedArtifacts: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -56,6 +61,9 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
+ mergedReportArtifacts() {
+ return [...this.reportArtifacts, ...this.injectedArtifacts];
+ },
},
methods: {
showError(error) {
@@ -77,7 +85,7 @@ export default {
<template>
<security-report-download-dropdown
:title="s__('SecurityReports|Download results')"
- :artifacts="reportArtifacts"
+ :artifacts="mergedReportArtifacts"
:loading="isLoadingReportArtifacts"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index b024e92bd0e..fafbd02634f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,6 +17,7 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_SAST_IAC = 'sast_iac';
export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_DAST_PROFILES = 'dast_profiles';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
index b5858ab012b..e1f3c55a886 100644
--- a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
@@ -1,6 +1,6 @@
#import "../fragments/job_artifacts.fragment.graphql"
-query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
+query getPipelineCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
id
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 62a51abe038..8aefc13a5fa 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -18,6 +18,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => {
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST)
.then((data) => {
dispatch('receiveDiffSuccess', data);
+ return data;
})
.catch(() => {
dispatch('receiveDiffError');
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
index 722dcce3075..13ca154bfa7 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
@@ -18,6 +18,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => {
return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION)
.then((data) => {
dispatch('receiveDiffSuccess', data);
+ return data;
})
.catch(() => {
dispatch('receiveDiffError');
diff --git a/app/assets/javascripts/work_items/components/app.vue b/app/assets/javascripts/work_items/components/app.vue
index 93de17d1e43..a14d0c32cbe 100644
--- a/app/assets/javascripts/work_items/components/app.vue
+++ b/app/assets/javascripts/work_items/components/app.vue
@@ -1,9 +1,5 @@
-<script>
-export default {
- name: 'WorkItemRoot',
-};
-</script>
-
<template>
- <div></div>
+ <div>
+ <router-view />
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
new file mode 100644
index 00000000000..b39f68abf74
--- /dev/null
+++ b/app/assets/javascripts/work_items/constants.js
@@ -0,0 +1,3 @@
+export const widgetTypes = {
+ title: 'TITLE',
+};
diff --git a/app/assets/javascripts/work_items/graphql/fragmentTypes.json b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..c048ac34ac0
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"INTERFACE","name":"WorkItemWidget","possibleTypes":[{"name":"TitleWidget"}]}]}}
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
new file mode 100644
index 00000000000..083735336ce
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import createDefaultClient from '~/lib/graphql';
+import workItemQuery from './work_item.query.graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
+import typeDefs from './typedefs.graphql';
+
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+export function createApolloProvider() {
+ Vue.use(VueApollo);
+
+ const defaultClient = createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ typeDefs,
+ },
+ );
+
+ defaultClient.cache.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: '1',
+ },
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: '1',
+ type: 'FEATURE',
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ contentText: 'Test Work Item Title',
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ return new VueApollo({
+ defaultClient,
+ });
+}
diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/resolvers.js
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index e69de29bb2d..4a6e4aeed60 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -0,0 +1,38 @@
+enum WorkItemType {
+ FEATURE
+}
+
+enum WidgetType {
+ TITLE
+}
+
+interface WorkItemWidget {
+ type: WidgetType!
+}
+
+# Replicating Relay connection type for client schema
+type WorkItemWidgetEdge {
+ cursor: String!
+ node: WorkItemWidget
+}
+
+type WorkItemWidgetConnection {
+ edges: [WorkItemWidgetEdge]
+ nodes: [WorkItemWidget]
+ pageInfo: PageInfo!
+}
+
+type TitleWidget implements WorkItemWidget {
+ type: WidgetType!
+ contentText: String!
+}
+
+type WorkItem {
+ id: ID!
+ type: WorkItemType!
+ widgets: [WorkItemWidgetConnection]
+}
+
+extend type Query {
+ workItem(id: ID!): WorkItem!
+}
diff --git a/app/assets/javascripts/work_items/graphql/widget.fragment.graphql b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
new file mode 100644
index 00000000000..d7608c26052
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/widget.fragment.graphql
@@ -0,0 +1,3 @@
+fragment WidgetBase on WorkItemWidget {
+ type
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
new file mode 100644
index 00000000000..549e4f8c65a
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -0,0 +1,16 @@
+#import './widget.fragment.graphql'
+
+query WorkItem($id: ID!) {
+ workItem(id: $id) @client {
+ id
+ type
+ widgets {
+ nodes {
+ ...WidgetBase
+ ... on TitleWidget {
+ contentText
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index a635d43776d..7cc8a23b7b1 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,11 +1,15 @@
import Vue from 'vue';
import App from './components/app.vue';
+import { createRouter } from './router';
+import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
return new Vue({
el,
+ router: createRouter(el.dataset.fullPath),
+ apolloProvider: createApolloProvider(),
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
new file mode 100644
index 00000000000..493ee0aba01
--- /dev/null
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -0,0 +1,48 @@
+<script>
+import workItemQuery from '../graphql/work_item.query.graphql';
+import { widgetTypes } from '../constants';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ workItem: null,
+ };
+ },
+ apollo: {
+ workItem: {
+ query: workItemQuery,
+ variables() {
+ return {
+ id: this.id,
+ };
+ },
+ },
+ },
+ computed: {
+ titleWidgetData() {
+ return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <!-- Title widget placeholder -->
+ <div>
+ <h2
+ v-if="titleWidgetData"
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5"
+ data-testid="title"
+ >
+ {{ titleWidgetData.contentText }}
+ </h2>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js
new file mode 100644
index 00000000000..142fab8cfa6
--- /dev/null
+++ b/app/assets/javascripts/work_items/router/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { routes } from './routes';
+
+Vue.use(VueRouter);
+
+export function createRouter(fullPath) {
+ return new VueRouter({
+ routes,
+ mode: 'history',
+ base: joinPaths(fullPath, '-', 'work_items'),
+ });
+}
diff --git a/app/assets/javascripts/work_items/router/routes.js b/app/assets/javascripts/work_items/router/routes.js
new file mode 100644
index 00000000000..a3cf44ad4ca
--- /dev/null
+++ b/app/assets/javascripts/work_items/router/routes.js
@@ -0,0 +1,8 @@
+export const routes = [
+ {
+ path: '/:id',
+ name: 'work_item',
+ component: () => import('../pages/work_item_root.vue'),
+ props: true,
+ },
+];
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
index 01d13b30d2b..5a5f39a4b77 100644
--- a/app/assets/stylesheets/emoji_sprites.scss
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -7,111 +7,111 @@
background-position: -20px 0;
}
-.emoji-1F627 {
+.emoji-8ball {
background-position: 0 -20px;
}
-.emoji-8ball {
+.emoji-a {
background-position: -20px -20px;
}
-.emoji-a {
+.emoji-ab {
background-position: -40px 0;
}
-.emoji-ab {
+.emoji-abc {
background-position: -40px -20px;
}
-.emoji-abc {
+.emoji-abcd {
background-position: 0 -40px;
}
-.emoji-abcd {
+.emoji-accept {
background-position: -20px -40px;
}
-.emoji-accept {
+.emoji-aerial_tramway {
background-position: -40px -40px;
}
-.emoji-aerial_tramway {
+.emoji-airplane {
background-position: -60px 0;
}
-.emoji-airplane {
+.emoji-airplane_arriving {
background-position: -60px -20px;
}
-.emoji-airplane_arriving {
+.emoji-airplane_departure {
background-position: -60px -40px;
}
-.emoji-airplane_departure {
+.emoji-airplane_small {
background-position: 0 -60px;
}
-.emoji-airplane_small {
+.emoji-alarm_clock {
background-position: -20px -60px;
}
-.emoji-alarm_clock {
+.emoji-alembic {
background-position: -40px -60px;
}
-.emoji-alembic {
+.emoji-alien {
background-position: -60px -60px;
}
-.emoji-alien {
+.emoji-ambulance {
background-position: -80px 0;
}
-.emoji-ambulance {
+.emoji-amphora {
background-position: -80px -20px;
}
-.emoji-amphora {
+.emoji-anchor {
background-position: -80px -40px;
}
-.emoji-anchor {
+.emoji-angel {
background-position: -80px -60px;
}
-.emoji-angel {
+.emoji-angel_tone1 {
background-position: 0 -80px;
}
-.emoji-angel_tone1 {
+.emoji-angel_tone2 {
background-position: -20px -80px;
}
-.emoji-angel_tone2 {
+.emoji-angel_tone3 {
background-position: -40px -80px;
}
-.emoji-angel_tone3 {
+.emoji-angel_tone4 {
background-position: -60px -80px;
}
-.emoji-angel_tone4 {
+.emoji-angel_tone5 {
background-position: -80px -80px;
}
-.emoji-angel_tone5 {
+.emoji-anger {
background-position: -100px 0;
}
-.emoji-anger {
+.emoji-anger_right {
background-position: -100px -20px;
}
-.emoji-anger_right {
+.emoji-angry {
background-position: -100px -40px;
}
-.emoji-angry {
+.emoji-anguished {
background-position: -100px -60px;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 06a8694eb3d..c1c8bfffff7 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -62,7 +62,6 @@
@import 'framework/sortable';
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
-@import 'framework/terms';
@import 'framework/read_more';
@import 'framework/flex_grid';
@import 'framework/system_messages';
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index df78543f96d..8f65f349cf9 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -146,13 +146,6 @@
* Blame file
*/
&.blame {
- //
- // IMPORTANT PERFORMANCE OPTIMIZATION
- //
- // When viewinng a blame with many commits a lot of content is rendered on the page.
- // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
- content-visibility: auto;
-
table {
border: 0;
margin: 0;
@@ -167,12 +160,6 @@
}
td {
- //
- // IMPORTANT PERFORMANCE OPTIMIZATION
- //
- // When viewinng a blame with many commits a lot of content is rendered on the page.
- // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
- content-visibility: auto;
border-top: 0;
border-bottom: 0;
@@ -235,6 +222,25 @@
color: $gray-900;
}
}
+
+ //
+ // IMPORTANT PERFORMANCE OPTIMIZATION
+ //
+ // When viewinng a blame with many commits a lot of content is rendered on the page.
+ // content-visibility rules below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
+ .commit {
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 3em;
+ }
+
+ code .line {
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 1.1875rem;
+ }
+
+ .line-numbers {
+ content-visibility: auto;
+ }
}
&.logs {
diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss
index 05991bc16fd..7dd0ae47834 100644
--- a/app/assets/stylesheets/framework/kbd.scss
+++ b/app/assets/stylesheets/framework/kbd.scss
@@ -12,4 +12,20 @@ kbd {
border-image: none;
border-radius: 3px;
box-shadow: 0 -1px 0 var(--gray-200, $gray-200) inset;
+
+ &.flat {
+ color: $code-color;
+ background-color: $gray-100;
+ border-color: var(--gray-10, $gray-10) var(--gray-10, $gray-10) var(--gray-50, $gray-50);
+ box-shadow: none;
+ border-radius: $border-radius-default;
+ font-family: $monospace-font;
+ font-size: $gl-font-size-small;
+ line-height: 1;
+ white-space: pre-wrap;
+ // Safari
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ word-break: keep-all;
+ }
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index ef294635641..9b04b9a2612 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -82,11 +82,11 @@
.nav-links {
li.md-header-toolbar {
margin-left: auto;
- display: none;
+ visibility: hidden;
padding-bottom: $gl-padding-8;
&.active {
- display: block;
+ visibility: visible;
@include media-breakpoint-down(xs) {
flex: none;
@@ -116,7 +116,7 @@
}
.md-preview-holder {
- min-height: 167px;
+ min-height: 172px;
padding: 10px 0;
overflow-x: auto;
}
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
deleted file mode 100644
index b07d6023127..00000000000
--- a/app/assets/stylesheets/framework/terms.scss
+++ /dev/null
@@ -1,60 +0,0 @@
-.terms {
- .with-performance-bar & {
- margin-top: 0;
- }
-
- .alert-wrapper {
- min-height: $header-height + $gl-padding;
- }
-
- .content {
- padding-top: $gl-padding;
- }
-
- .card {
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- line-height: $line-height-base;
-
- .logo-text {
- width: 55px;
- height: 24px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
-
- .navbar-collapse {
- padding-right: 0;
- flex-grow: 0;
- flex-basis: auto;
-
- .navbar-nav {
- margin: 0;
- }
- }
-
- .nav li {
- float: none;
- }
- }
-
- .panel-content {
- padding: $gl-padding;
-
- *:first-child {
- margin-top: 0;
- }
-
- *:last-child {
- margin-bottom: 0;
- }
- }
-
- .footer-block {
- margin: 0;
- }
- }
-}
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 8270db9966e..fb4266a2f41 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -96,9 +96,10 @@
}
@mixin line-number-link($color) {
+ min-width: $gl-spacing-scale-9;
+
&::before {
- @include gl-visibility-hidden;
- @include gl-display-inline-block;
+ @include gl-display-none;
@include gl-align-self-center;
@include gl-mt-2;
@include gl-mr-2;
@@ -114,10 +115,10 @@
}
&:hover::before {
- @include gl-visibility-visible;
+ @include gl-display-inline-block;
}
&:focus::before {
- @include gl-visibility-visible;
+ @include gl-display-inline-block;
}
}
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index 3220510775c..5f50489555b 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -143,6 +143,10 @@ table.content {
line-height: 1.4;
padding: 15px 5px;
text-align: center;
+
+ ul.list-style-position-inside {
+ list-style-position: inside;
+ }
}
td.mailer-align-left {
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index a3ec2167b13..d4c59a6ab0c 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -470,6 +470,10 @@
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
width: auto;
}
+
+ .show.dropdown .dropdown-menu {
+ @include gl-w-full;
+ }
}
.board-header-collapsed-info-icon:hover {
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 4beb5edbe7b..9fe0490571e 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -42,8 +42,6 @@ $header-height: 40px;
.jira-connect-app-body {
max-width: 768px;
- margin-left: auto;
- margin-right: auto;
}
// needed for external_link
diff --git a/app/assets/stylesheets/page_bundles/terms.scss b/app/assets/stylesheets/page_bundles/terms.scss
new file mode 100644
index 00000000000..8eb66e58aed
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/terms.scss
@@ -0,0 +1,64 @@
+@import 'mixins_and_variables_and_functions';
+
+.terms {
+ .with-system-header &,
+ .with-system-header.with-performance-bar &,
+ .with-performance-bar & {
+ margin-top: 0;
+ }
+
+ .terms-fade {
+ background: linear-gradient(0deg, $white 0%, rgba($white, 0.5) 100%);
+ }
+
+ .content {
+ padding-top: $gl-padding;
+ }
+
+ .gl-card {
+ .gl-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ line-height: $line-height-base;
+
+ .logo-text {
+ width: 55px;
+ height: 24px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .navbar-collapse {
+ padding-right: 0;
+ flex-grow: 0;
+ flex-basis: auto;
+
+ .navbar-nav {
+ margin: 0;
+ }
+ }
+
+ .nav li {
+ float: none;
+ }
+ }
+
+ .panel-content {
+ padding: $gl-padding;
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .footer-block {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index de27ca2e5e8..b450bca4f41 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -1,9 +1,13 @@
.clusters-container {
- .empty-state .svg-content img {
- width: 145px;
+ .empty-state .svg-content {
+ @include gl-pb-0;
+
+ img {
+ width: 100px;
+ }
}
- .empty-state--agent {
+ .agents-empty-state {
.text-content {
@include gl-max-w-full;
@include media-breakpoint-up(lg) {
@@ -16,4 +20,23 @@
@include gl-flex-wrap;
}
}
+
+ .gl-card-body {
+ @include media-breakpoint-up(sm) {
+ @include gl-pt-2;
+ min-height: 372px;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .nav-controls {
+ @include gl-w-full;
+ order: -1;
+
+ .gl-new-dropdown,
+ .split-content-button {
+ @include gl-w-full;
+ }
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
index 2fafe052106..997e42a8fd5 100644
--- a/app/assets/stylesheets/pages/deploy_keys.scss
+++ b/app/assets/stylesheets/pages/deploy_keys.scss
@@ -1,12 +1,3 @@
-.deploy-keys-list {
- width: 100%;
- overflow: auto;
-
- table {
- border: 1px solid $table-border-color;
- }
-}
-
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index c597d2dd8da..cf5e93e94a2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -179,6 +179,7 @@
}
.block,
+ .sidebar-contained-width,
.issuable-sidebar-header {
@include clearfix;
padding: $gl-padding 0;
@@ -317,6 +318,7 @@
padding: 0;
.block,
+ .sidebar-contained-width,
.issuable-sidebar-header {
width: $gutter-collapsed-width - 2px;
padding: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index cec8d8a29cc..3b86750c6ca 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -269,7 +269,9 @@ $tabs-holder-z-index: 250;
}
.mr-widget-body {
- line-height: 28px;
+ &:not(.mr-widget-body-line-height-1) {
+ line-height: 28px;
+ }
@include clearfix;
diff --git a/app/assets/stylesheets/startup/_cloaking.scss b/app/assets/stylesheets/startup/_cloaking.scss
index 3c25feb0c5c..f60d72a51fb 100644
--- a/app/assets/stylesheets/startup/_cloaking.scss
+++ b/app/assets/stylesheets/startup/_cloaking.scss
@@ -2,6 +2,8 @@
Prevent flashing of content when using startup.css
*/
@mixin cloak-startup-scss($display) {
+ // General selector for cloaking until ready
+ .cloak-startup,
// Breadcrumbs and alerts on the top of the page
.content-wrapper > .alert-wrapper,
// Content on pages
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index d436c328921..efa4b04ee62 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -5,6 +5,7 @@
body.gl-dark {
--gray-50: #303030;
--gray-100: #404040;
+ --gray-600: #bfbfbf;
--gray-900: #fafafa;
--gray-950: #fff;
--green-100: #0d532a;
@@ -12,6 +13,7 @@ body.gl-dark {
--green-700: #91d4a8;
--blue-400: #1f75cb;
--orange-400: #ab6100;
+ --indigo-900-alpha-008: rgba(235, 235, 250, 0.08);
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--black: #fff;
@@ -1693,9 +1695,15 @@ body.gl-dark {
--black: #fff;
--svg-status-bg: #333;
}
+.nav-sidebar li a {
+ color: var(--gray-600);
+}
.nav-sidebar li.active {
box-shadow: none;
}
+.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
+ background-color: var(--indigo-900-alpha-008);
+}
body.gl-dark .navbar-gitlab {
background-color: #fafafa;
}
@@ -1780,7 +1788,7 @@ body.gl-dark .search .search-input-wrap .clear-icon {
fill: rgba(250, 250, 250, 0.8);
}
body.gl-dark .nav-sidebar li.active > a {
- color: #f0f0f0;
+ color: #fafafa;
}
body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
@@ -1935,7 +1943,7 @@ body.gl-dark {
.gl-display-none {
display: none;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 40026c95a15..977f994dc78 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -1621,7 +1621,7 @@ svg.s16 {
.gl-display-none {
display: none;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 8d7531d6c9c..3daeeb30082 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -776,7 +776,7 @@ svg {
.gl-mb-5 {
margin-bottom: 1rem;
}
-@media (min-width: 36rem) {
+@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;
}
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index b77048174c9..2b5751cab36 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -42,8 +42,24 @@
}
.nav-sidebar {
- li.active {
- box-shadow: none;
+ li {
+ a {
+ color: var(--gray-600);
+ }
+
+ > a:hover {
+ background-color: var(--indigo-900-alpha-008);
+ }
+
+ &.active {
+ box-shadow: none;
+
+ &:not(.fly-out-top-item) {
+ > a:not(.has-sub-items) {
+ background-color: var(--indigo-900-alpha-008);
+ }
+ }
+ }
}
.sidebar-sub-level-items.fly-out-list {
@@ -53,7 +69,7 @@
}
body.gl-dark {
- @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-800, $gray-900, $white);
+ @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);
.logo-text svg {
fill: var(--gl-text-color);
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 9f9802f77f4..817557f37cd 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -6,7 +6,7 @@ body {
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
- $theme-blue-800,
+ $gray-900,
$theme-blue-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_dark.scss b/app/assets/stylesheets/themes/theme_dark.scss
index e6db6cd2a5e..4c52cdc30df 100644
--- a/app/assets/stylesheets/themes/theme_dark.scss
+++ b/app/assets/stylesheets/themes/theme_dark.scss
@@ -6,7 +6,7 @@ body {
$gray-200,
$gray-300,
$gray-500,
- $gray-700,
+ $gray-900,
$gray-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index 6dcad6e1301..7e387e97452 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-700,
- $theme-green-800,
+ $gray-900,
$theme-green-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index 9566c9c6004..3bf6cfea650 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -6,7 +6,7 @@ body {
$indigo-200,
$indigo-500,
$indigo-700,
- $purple-900,
+ $gray-900,
$indigo-900,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss
index 4c3bc1b2298..f2fdd499781 100644
--- a/app/assets/stylesheets/themes/theme_light.scss
+++ b/app/assets/stylesheets/themes/theme_light.scss
@@ -6,7 +6,7 @@ body {
$gray-500,
$gray-700,
$gray-500,
- $gray-500,
+ $gray-900,
$gray-50,
$gray-500
);
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 07d1c60a4c6..771a84911b3 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -6,7 +6,7 @@ body {
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
- $theme-light-blue-700,
+ $gray-900,
$theme-light-blue-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index e122501b93c..8c991a7bfb3 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-500,
- $theme-light-green-700,
+ $gray-900,
$theme-light-green-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index 5b607238ed9..6c220e0459a 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -6,7 +6,7 @@ body {
$indigo-200,
$indigo-500,
$indigo-500,
- $indigo-700,
+ $gray-900,
$indigo-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index fd3980183f3..e1a715293b4 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -6,7 +6,7 @@ body {
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
- $theme-light-red-700,
+ $gray-900,
$theme-light-red-700,
$white
);
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index fa5ecc09f50..19fd150727d 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -6,7 +6,7 @@ body {
$theme-red-200,
$theme-red-500,
$theme-red-700,
- $theme-red-800,
+ $gray-900,
$theme-red-900,
$white
);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index ec70926b418..7e46f16e1d0 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -281,3 +281,19 @@ $gl-line-height-42: px-to-rem(42px);
display: none;
}
}
+
+// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
+.gl-text-transparent {
+ color: transparent;
+}
+
+.gl-focus-ring-border-1-gray-900\! {
+ @include gl-focus($gl-border-size-1, $gray-900, true);
+}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2476
+.gl-md-max-w-50p {
+ @include gl-media-breakpoint-up(md) {
+ max-width: 50%;
+ }
+}