Compare commits

..

263 Commits

Author SHA1 Message Date
SabreCat e383614107 4.242.1 2022-09-01 14:47:20 -05:00
Natalie L 2b21410abd fix(quest shop): correct forest friends end date (#14202) 2022-09-01 14:45:49 -05:00
Weblate 56f956be5a Merge branch 'origin/develop' into Weblate. 2022-08-30 21:25:56 +02:00
Weblate 2b44d32b1c Translated using Weblate (Ukrainian)
Currently translated at 39.3% (81 of 206 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.1% (311 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 94.8% (369 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.9% (118 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 78.6% (565 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.4% (174 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 64.0% (132 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (English (Pirate) (en@pirate))

Currently translated at 88.5% (124 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 43.8% (79 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 41.1% (74 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (30 of 58 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Korean)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 63.5% (131 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Czech)

Currently translated at 74.4% (1977 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.7% (100 of 127 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (French)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.5% (360 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Czech)

Currently translated at 74.2% (1972 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 98.9% (93 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 73.8% (1962 of 2655 strings)

Translated using Weblate (Romanian)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 72.8% (1934 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Romanian)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Romanian)

Currently translated at 90.0% (126 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Italian)

Currently translated at 99.2% (386 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (354 of 367 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Filipino)

Currently translated at 81.3% (109 of 134 strings)

Translated using Weblate (Filipino)

Currently translated at 95.0% (133 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 36.3% (8 of 22 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.8% (214 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.4% (204 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (2642 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (2640 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 50.0% (11 of 22 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.6% (602 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (2636 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.6% (786 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Filipino)

Currently translated at 72.9% (551 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (135 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 93.4% (200 of 214 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.8% (1163 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.8% (198 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (2628 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.0% (108 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 57.8% (1535 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2623 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.4% (197 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.7% (1667 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 72.1% (101 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.1% (56 of 206 strings)

Translated using Weblate (French)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.4% (1951 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 40.9% (309 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Korean)

Currently translated at 92.3% (339 of 367 strings)

Translated using Weblate (Filipino)

Currently translated at 86.3% (620 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 74.7% (564 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 74.2% (104 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.6% (96 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.5% (2005 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 75.3% (569 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 78.5% (110 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Dutch)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.9% (490 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.3% (554 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.8% (148 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.9% (180 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.7% (2010 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.1% (173 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.5% (44 of 56 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.6% (594 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (114 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 90.0% (162 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (206 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (108 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 86.1% (316 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1783 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 72.2% (39 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 91.4% (43 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 78.6% (169 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 92.8% (52 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (107 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.8% (119 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (4 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 12.1% (17 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 83.6% (51 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 77.2% (17 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 85.8% (315 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1782 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (39 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 70.8% (90 of 127 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2589 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Filipino)

Currently translated at 84.6% (94 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 79.2% (111 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 94.1% (351 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 86.4% (96 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 82.1% (115 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 82.8% (116 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 83.5% (117 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.9% (173 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (903 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2588 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (French)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2587 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.4% (120 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.6% (2647 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.9% (113 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 79.5% (101 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2586 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2583 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2581 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 87.3% (180 of 206 strings)

Translated using Weblate (French)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.0% (2576 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 86.8% (179 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Korean)

Currently translated at 74.8% (95 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2632 of 2655 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.7% (702 of 718 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (French)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.7% (2568 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 82.4% (113 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 42.3% (1125 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 38.3% (69 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 90.1% (193 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 96.5% (2563 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (214 of 215 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (2559 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (107 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 89.9% (116 of 129 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 76.6% (105 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 64.4% (1711 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 83.3% (45 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (44 of 54 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 44.6% (25 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 95.6% (178 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1784 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (93 of 111 strings)

Translated using Weblate (Galician)

Currently translated at 21.1% (38 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (179 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 89.5% (120 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 83.1% (628 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 13.8% (19 of 137 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 87.1% (320 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.3% (1789 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 89.8% (335 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 74.0% (94 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 76.7% (165 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 3.8% (7 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 96.4% (54 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 57.1% (410 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 58.7% (121 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 91.1% (195 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 73.2% (93 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 74.4% (160 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (754 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 75.9% (104 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Co-authored-by: Adriana Alupei <a.ady96@yahoo.com>
Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Anton de Regt <antonderegt@pm.me>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kővári <davson.kovari@gmail.com>
Co-authored-by: Felix Wittwer <spam@felixwittwer.de>
Co-authored-by: Forst Wolf <forestwolf@spam.care>
Co-authored-by: Forstwolf <forestwolf@spam.care>
Co-authored-by: Goggle K <afc731@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Hyun Yeol Kim <hyunyeolkim@gmail.com>
Co-authored-by: JMFO16 <fournier.olivera.jm@gmail.com>
Co-authored-by: Kedr <sergeysamori.ua@gmail.com>
Co-authored-by: Leslie Munguía <moongeeuh@gmail.com>
Co-authored-by: Linda Li <wli62442@gmail.com>
Co-authored-by: Martim Pinto Paiva <pintopaivam@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Richard Gould <rgould@u2622.ca>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Sara de Nicolas <saradenicolas12@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Tran Lam Van Khoa <lamvankhoat1@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Ventus Meigo <at.fbfzd@gmail.com>
Co-authored-by: Vince <vincemorel.vilan.889@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zero <leedambak@gmail.com>
Co-authored-by: datschka <datschka@gmx.at>
Co-authored-by: hekin zhou <1916360372@qq.com>
Co-authored-by: jiangshanghan <jsh1215@hash.fyi>
Co-authored-by: kat o(`ω´ )o <memesarerealkool@gmail.com>
Co-authored-by: neko kyuri <Nekorin0621@gmail.com>
Co-authored-by: parkbird <kgh9812@naver.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en@pirate/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/death/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/it/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/it/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-08-30 21:25:33 +02:00
SabreCat 11347e5679 Merge branch 'release' into develop 2022-08-30 14:25:25 -05:00
SabreCat a04479e689 4.242.0 2022-08-30 14:21:26 -05:00
SabreCat 4ce4e55e80 chore(git): update subproject commit 2022-08-30 14:20:50 -05:00
Natalie L 755f51b674 chore(content): add September 2022 Mystery Items (#14199)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add September 2022 Mystery Items

* fix(typo): verb form

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-30 14:06:06 -05:00
SabreCat 6c1b21117f fix(teams): Stealth calc and small screens 2022-08-29 17:09:06 -05:00
SabreCat 14441701c9 Merge branch 'release' into develop 2022-08-29 15:51:45 -05:00
Anton de Regt ccb821fd6f Fix redirect after register issue (#14182) 2022-08-26 15:56:58 -05:00
Natalie L 3664a1ebb1 fix(tavern): update Pause Damage Description, and Staff list (#14174) 2022-08-26 15:51:21 -05:00
Natalie L 509cb00374 fix(api): add API version (#14177) 2022-08-26 15:46:43 -05:00
theneelshah bc4770577a Fix prop change handler for guild challenges. (#14169)
Fix props' change handler which is called when guild is changed from
notification.

Tests:

+ Guild challenges updated successfully when guild is changed from
notification center.

Co-authored-by: neel <neel@helpshift.com>
2022-08-26 15:45:32 -05:00
Jason Mishi Carvalho f158852be5 Grey out skill when insufficient mana fixes #13286 (#14100)
* disable spell if user doesn't have enough mana

* differenciate insufficient mana and disabled spell

* linting

* reduce opacity, no hover state when insufficient mana

* display that lvl insufficient in spell tooltip

* change spell text color when spell has no effect

change spell-text color to blue-500 when spell has no effect
2022-08-26 15:22:04 -05:00
SabreCat ee0f6fd78f fix(test): same thing for v3 2022-08-25 10:35:49 -05:00
SabreCat 3284611bbf fix(test): adjust expectation for exploit fix 2022-08-25 10:22:16 -05:00
SabreCat 986d38af69 4.241.4 2022-08-25 09:24:17 -05:00
SabreCat 7129639bbf fix(misc): correct one groups issue and two others 2022-08-25 09:24:08 -05:00
SabreCat 6aabf7b19a 4.241.3 2022-08-24 14:18:20 -05:00
SabreCat a5d9448af1 fix(cron): don't process group tasks during user cron 2022-08-24 14:18:07 -05:00
SabreCat 6e19a0ef2e Merge branch 'release' into develop 2022-08-24 11:06:11 -05:00
SabreCat bc8b1884b7 4.241.2 2022-08-24 11:05:36 -05:00
SabreCat 1aae9638ec fix(tasks): address regressions from group plan rollout 2022-08-24 11:05:19 -05:00
dependabot[bot] e6b0c1e488 build(deps): bump @babel/core from 7.18.10 to 7.18.13 (#14184)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.10 to 7.18.13.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.13/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-23 19:57:59 -04:00
dependabot[bot] d5bbc9599c build(deps): bump core-js from 3.23.5 to 3.24.1 in /website/client (#14154)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.5 to 3.24.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.23.5...v3.24.1)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-23 19:49:41 -04:00
SabreCat a2f191089c 4.241.1 2022-08-23 14:38:09 -05:00
SabreCat 75c8486b1a fix(checklists): allow scoring own items 2022-08-23 14:38:05 -05:00
Sabe Jones 20854057ad fix(migration): handle orphaned assignments 2022-08-23 19:18:01 +00:00
SabreCat ae3f064197 4.241.0 2022-08-23 12:49:25 -05:00
SabreCat 67ee0b72d3 fix(tasks): don't show reset counter control on group tasks 2022-08-23 12:40:32 -05:00
SabreCat aebf13810f fix(tasks): remove spurious uncheck notification 2022-08-23 11:33:06 -05:00
SabreCat 971891dd6b fix(tasks): no really, address not-found error 2022-08-23 10:48:21 -05:00
SabreCat 395b8db932 fix(tasks): fix unassigned error case 2022-08-23 09:54:44 -05:00
SabreCat da5c3f9602 fix(tasks): address not-found error on open uncheck 2022-08-23 09:36:23 -05:00
SabreCat 4c85b933cb fix(tests): correct one last v3 test and wrap v4 2022-08-22 21:59:51 -05:00
SabreCat 82abdaa0c4 WIP(tests): finish cleaning up v3 integrations 2022-08-22 21:47:55 -05:00
SabreCat 02c50b6126 WIP(tests): fix various assign requests and needs-work flow 2022-08-22 20:45:22 -05:00
SabreCat 3ab88bbb3f fix(tests): short circuit getter, adjust expectations 2022-08-22 18:49:56 -05:00
SabreCat 5251598369 fix(cron): fix score down breaking during middleware 2022-08-22 16:38:51 -05:00
SabreCat 149da578fd fix(teams): fix fix fix
Removed testing banner
Fixed a JS console error when assigning a user to a previously open task
Fixed a potential abuse where user might be able to score someone else's 
task via API call
Fixed an issue where finding tasks by alias could return tasks belonging 
to other users
Fixed an issue that was appending the user's party ID to their list of 
Guilds
Fixed an issue where group tasks were not receiving the default tag 
needed for filtering them on user's personal list
2022-08-22 16:16:23 -05:00
SabreCat 35d963a397 fix(teams): tweak FAQ and fix sync test 2022-08-22 14:43:21 -05:00
SabreCat cccd8c3b1b Revert "fix(tests): catch non-array parameter"
This reverts commit 595c131398.
2022-08-22 14:17:06 -05:00
SabreCat 631d7111a5 fix(teams): send @username in notifications 2022-08-22 11:39:53 -05:00
SabreCat 89c07529ea feat(teams): add FAQ entry
Also a few client side fixes
2022-08-22 11:27:08 -05:00
SabreCat 595c131398 fix(tests): catch non-array parameter 2022-08-19 17:13:48 -05:00
SabreCat f063b9e81c fix(tests): sanity and common 2022-08-19 16:10:18 -05:00
SabreCat 49a20218a5 fix(onboarding): improve modal launching and clicky behavior 2022-08-19 14:09:41 -05:00
SabreCat 95714599f0 feat(onboarding): welcome modal 2022-08-19 13:04:48 -05:00
SabreCat 5893312d75 Merge branch 'develop' into sabrecat/teams-rebase 2022-08-17 14:28:43 -05:00
SabreCat 71fa4d6cb7 Merge branch 'release' into develop 2022-08-15 15:36:18 -05:00
SabreCat 922b2e985a 4.240.0 2022-08-15 15:29:12 -05:00
SabreCat b82239811c chore(content): add items to featured 2022-08-15 15:28:47 -05:00
Natalie L f0b5637e9e chore(content): add Porcelain Magic Hatching Potion (#14168)
* chore(submodule): add August 2022 Mystery Items

* chore: update habitica-images

* chore(content): add Porcelain Magic Hatching Potion

* chore(content): update moonglow potion availability

* fix(events): no gap between events

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-15 15:14:23 -05:00
dependabot[bot] 2c93b3e2e3 build(deps): bump @babel/preset-env from 7.18.6 to 7.18.10 (#14158)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.18.6 to 7.18.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 13:31:37 -04:00
dependabot[bot] 72a9417de9 build(deps): bump @babel/core from 7.18.6 to 7.18.10 (#14157)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.6 to 7.18.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.10/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 13:06:55 -04:00
SabreCat 1701fc702b Merge branch 'develop' into sabrecat/teams-rebase 2022-08-09 11:55:46 -05:00
SabreCat 16dc6a1b4c Merge branch 'release' into develop 2022-08-09 11:54:45 -05:00
SabreCat d3a91aab72 4.239.0 2022-08-09 11:52:26 -05:00
Natalie L 0528ee1761 fix(dates): update end dates for quest bundle (#14167)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add Woodland Wizard achievement

* chore(content): add Forest Friends quest bundle

* fix(typo): whitespace

* fix(dates): update end date for quest bundle

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-09 11:50:05 -05:00
SabreCat 1b4d670b0a fix(tasks): styles and wordings 2022-08-08 15:50:37 -05:00
SabreCat 3654e01fee Merge branch 'develop' into sabrecat/teams-rebase 2022-08-08 12:13:29 -05:00
SabreCat fdbeda19e2 fix(tasks): icons and select lists 2022-08-08 12:13:15 -05:00
dependabot[bot] db723d79a4 build(deps): bump dompurify from 2.3.8 to 2.3.10 in /website/client (#14132)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.8 to 2.3.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.8...2.3.10)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:22:23 -04:00
dependabot[bot] d5d1bfbd99 build(deps): bump terser from 4.6.7 to 4.8.1 in /website/client (#14133)
Bumps [terser](https://github.com/terser/terser) from 4.6.7 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:21:48 -04:00
dependabot[bot] d06f4f4e1e build(deps-dev): bump @babel/plugin-proposal-optional-chaining (#14135)
Bumps [@babel/plugin-proposal-optional-chaining](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-optional-chaining) from 7.18.6 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/packages/babel-plugin-proposal-optional-chaining)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-optional-chaining"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:21:20 -04:00
dependabot[bot] 82c4260fca build(deps-dev): bump run-rs from 0.7.6 to 0.7.7 (#14138)
Bumps [run-rs](https://github.com/vkarpov15/run-rs) from 0.7.6 to 0.7.7.
- [Release notes](https://github.com/vkarpov15/run-rs/releases)
- [Changelog](https://github.com/vkarpov15/run-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vkarpov15/run-rs/compare/0.7.6...0.7.7)

---
updated-dependencies:
- dependency-name: run-rs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:57 -04:00
dependabot[bot] 0f3a26a490 build(deps): bump @babel/register from 7.18.6 to 7.18.9 (#14140)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.18.6 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:40 -04:00
dependabot[bot] 9c2963e557 build(deps): bump vue and vue-template-compiler in /website/client (#14143)
Bumps [vue](https://github.com/vuejs/core) and [vue-template-compiler](https://github.com/vuejs/vue). These dependencies needed to be updated together.

Updates `vue` from 2.6.14 to 2.7.8
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits)

Updates `vue-template-compiler` from 2.6.14 to 2.7.8
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.6.14...v2.7.8)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: vue-template-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:08 -04:00
SabreCat d5c4e1666e 4.238.1 2022-08-04 16:19:03 -05:00
SabreCat 79071e3445 Merge branch 'develop' into release 2022-08-04 16:18:58 -05:00
Weblate 2ea707c27c Translated using Weblate (Korean)
Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 64.4% (1711 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (2558 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (367 of 367 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.2% (2551 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 96.1% (2549 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.4% (117 of 137 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2651 of 2651 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 98.9% (184 of 186 strings)

Translated using Weblate (Filipino)

Currently translated at 42.3% (1124 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (2633 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (751 of 755 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2651 of 2651 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (204 of 205 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (367 of 367 strings)

Translated using Weblate (Russian)

Currently translated at 96.1% (2548 of 2651 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2648 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (136 of 137 strings)

Translated using Weblate (Russian)

Currently translated at 98.6% (212 of 215 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Catarina Rocha <caticalhau312@gmail.com>
Co-authored-by: Goggle K <afc731@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Vince <vincemorel.vilan.889@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: datschka <datschka@gmx.at>
Co-authored-by: kat <memesarerealkool@gmail.com>
Co-authored-by: kat o(`ω´ )o <memesarerealkool@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2022-08-04 23:17:51 +02:00
Natalie L f7b727dc95 chore(content): add Woodland Wizard Achievement and Forest Friends Quest Bundle (#14159)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add Woodland Wizard achievement

* chore(content): add Forest Friends quest bundle

* fix(typo): whitespace

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-04 15:14:27 -05:00
SabreCat 9b9503b141 feat(tasks): make task copy/mirror pref per-group 2022-08-02 16:09:49 -05:00
SabreCat a78aea5456 Merge branch 'release' into develop 2022-08-02 10:10:00 -05:00
SabreCat 6e8e7318f3 4.238.0 2022-08-02 10:09:31 -05:00
SabreCat 1c3d4a6fd5 Revert "Revert "chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)""
This reverts commit 9a2b49b4bf.
2022-08-02 10:09:02 -05:00
SabreCat a418752041 Merge branch 'develop' into sabrecat/teams-rebase 2022-08-01 15:40:08 -05:00
negue c9b3c48379 fixed gifting transaction / adding comments (#14150) 2022-08-01 11:10:00 -05:00
SabreCat 9ed2359c77 4.237.1 2022-08-01 08:51:49 -05:00
SabreCat 0dc21fa868 fix(css): redo sprites compile 2022-08-01 08:51:44 -05:00
Natalie L e2c6fb1ea2 chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)
* chore(submodule): add August 2022 Mystery Items

* chore(content): August 2022 Backgrounds and Enchanted Armoire Items

* chore(submodule): August 2022 Backgrounds and Enchanted Armoire images

* fix(typo): space

* fix(whitespace): spaces

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-07-29 16:51:56 -05:00
SabreCat b87527bcea 4.237.0 2022-07-29 16:26:29 -05:00
SabreCat 0adfc9f756 chore(subproject): update habitica-images 2022-07-29 16:26:15 -05:00
SabreCat 9a2b49b4bf Revert "chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)"
This reverts commit 2748455f16.
2022-07-29 16:25:14 -05:00
SabreCat 750a02053c feat(content): August 2022 subscriber items
Code by @CuriousMagpie
2022-07-29 16:24:32 -05:00
Natalie L 2748455f16 chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)
* chore(submodule): add August 2022 Mystery Items

* chore(content): August 2022 Backgrounds and Enchanted Armoire Items

* chore(submodule): August 2022 Backgrounds and Enchanted Armoire images

* fix(typo): space

* fix(whitespace): spaces

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-07-29 16:21:20 -05:00
SabreCat 7f8e44ff49 fix(groups): style and data sync fixes 2022-07-28 15:53:29 -05:00
SabreCat e0e9381584 fix(groups): many, mostly style, fixes 2022-07-26 16:48:27 -05:00
CuriousMagpie ef3767f80b chore(submodule): add August 2022 Mystery Items 2022-07-26 13:31:44 -04:00
SabreCat 18db432f7f feat(tasks): functional summary modal 2022-07-25 22:31:33 -05:00
SabreCat 30d3892fb4 Merge branch 'develop' into sabrecat/teams-rebase 2022-07-25 15:04:43 -05:00
Nishant Jain 8070486def add max length validations for summary in challenge create and update… (#14053)
* add max length validations for summary in challenge create and update controllers

* Add validation to group APIs

* fix lint errors

* add validation to group plan

* fix imports

* add tests

* add max length validations for summary in challenge create and update controllers

* Add validation to group APIs

* fix lint errors

* add validation to group plan

* fix imports

* add tests

* lint checks
2022-07-22 15:24:24 -05:00
SabreCat cc0e807609 4.236.3 2022-07-21 15:36:17 -05:00
SabreCat bbc5a54a3e Merge branch 'develop' into release 2022-07-21 15:36:13 -05:00
SabreCat e06a0e5e7f WIP(tasks): new summary modal 2022-07-21 15:35:59 -05:00
Sabe Jones 8e717de039 Server setting to disallow chat from new accounts (#13952)
* feat(chat): server setting to disallow chat from new accounts

* fix(tests): many adjustments to handle chat minimum age

* fix(tests): address issues outside of chat posting

* chore(analytics): add incident logging

* fix(config): allow instant chat for dev purposes

* fix(test): finely age one more user

* fix(test): member not leader

Co-authored-by: SabreCat <sabe@habitica.com>
2022-07-21 15:32:28 -05:00
Natalie L ce18e614be Gifting modal design - amazonModal.vue update (#14131)
* update selectUserModal.vue

* more updates to selectUserModal.vue, typo fix in subscriber.json

* remove exact sizing for selectUserModal.vue

* update to size for selectUserModal.vue

* added sendGiftModal.vue file

* updates to selectUser & sendGift modals

* making the modals go & position cursor

* working working working

* added a return to method

* avatar display & placeholder profile.name and username

* subscription-options added

* added menu row & started on gem options

* Added selectPage function, have not tested.

* updated habitica-images

* state changes

* bringing in gem counter

* arranging elements

* state changes, gem input boxes

* styling sendGiftModal.vue

* more sendGiftModal.vue styling and new close.svg icon

* more styling!

* and more styling of send own gems part of page

* images update

* more styling of own gems & some attempts to adjust :class on the menu

* styling styling styling

* replace +/- svg, styling

* styling, mostly

* new SVGs

* stylin'

* reverting svg changes

* no more stylin'

* finally got the +/- icons to show up...but they're the wrong color

* solved svg icon color problem! :)

* habitica-images

* working on sendGift part of button

* trying to make it do math, failing

* more attempts at math

* +/- buttons work on gem pages & cost calculation on buyGems

* trying to get hover colors working on +/- svgs

* formatted dollar amount as currency

* css/html for subscription-options & payments-buttons simplified

* swag at payments-buttons parameter (not tested)

* send gems from own balance works!

* working on starting page

* increment gem amount limited to maxGems and not < 0

* uncommented onHide()

* got bg color on sub options to work! yay!

* payment buttons!

* making g1g1 look good

* position modal on page properly & code clean-up

* Changes as requested!

* small color update

* fixed ternary function

* chore(html): indentation and comments

* fix(fn): correct catch for under-0

* chore(json): whitespace

* update gem styling; add linebreak to notifications.vue bc linter

* updating subscriptionOptions

* snackbar css fix

* reverting commit e16c12f

* removing merge conflict markers

* just a little comment

* fixed some navigation, clear input field on selectPage, cleaned up code; another try at subscriptionOption.vue

* merge upstream/develop

* update selectPage() to disable Gems menu items when on 'ownGems' or 'buyGems' states

* working on subscriptionOptions.vue logic

* fix(script): changed props & added updateSubscriptionData()

* fix(script): forgot to call updateSubscriptionData()

* fix(scripts): corrected :userReceivingGift on sendGiftModal.vue

* fix(scripts): correct props userReceivingGift to an Object

* fix(scripts): corrected v-if & revised props

* fix(style/html/whitespace): updated css for close.svg and added missing </div>

* style(radio-buttons): updated focus states and added hover states

* style(radio-buttons): refined focus and hover states

* fix(function): changed buyGemsLink to buyGems; still working on menu

* style(radio buttons): ensured consistent display of radio buttons through-out site; still struggling with hover states

* style(radio buttons): updated focus/active/hover to match design & removed unnecessary code

* fix: set default subscription option to 1 month

* fix(function): add default amounts to gem states when modal selected from user profile

* fix(build): use develop package json

* fix: SCSS commenting & abstracted setGemsDefault()

* fix(packages): revert to develop

* fix: remove unnecessary console.log statement

* fix(payments): storePaymentStatusAndReload() modified

Co-authored-by: SabreCat <sabe@habitica.com>
2022-07-21 14:06:50 -05:00
dependabot[bot] 58c27c2610 build(deps): bump apidoc from 0.51.1 to 0.52.0 (#14126)
Bumps [apidoc](https://github.com/apidoc/apidoc) from 0.51.1 to 0.52.0.
- [Release notes](https://github.com/apidoc/apidoc/releases)
- [Changelog](https://github.com/apidoc/apidoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apidoc/apidoc/compare/0.51.1...0.52.0)

---
updated-dependencies:
- dependency-name: apidoc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-18 17:03:16 -04:00
dependabot[bot] 220dd51f85 build(deps): bump core-js from 3.23.4 to 3.23.5 in /website/client (#14127)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.4 to 3.23.5.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.23.4...v3.23.5)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-18 17:02:59 -04:00
SabreCat 153561dd42 Merge branch 'develop' into sabrecat/teams-rebase 2022-07-18 15:50:20 -05:00
Vi Mio 3b1407f529 feat: prevent user from purchasing a quest if prerequisites are not met (#14073)
* feat: prevent user from purchasing a quest if prerequisites are not met

* test: fail to buy quest if not all prerequisites are met

* test: modify to check all quest prerequisites
2022-07-14 15:24:52 -05:00
SabreCat be7b3076eb 4.236.2 2022-07-13 14:52:10 -05:00
negue 2b4ffdf27f filter out bank challenge if is not userSupport 2022-07-13 14:52:05 -05:00
negue e0dc608fd8 Transaction Logs - Backend Changes 2022-07-13 14:51:56 -05:00
negue 0b4059aab0 Transaction Logs - Backend Changes (#14113)
* Transaction Logs - Backend Changes

* filter out bank challenge if is not userSupport
2022-07-13 14:18:59 -05:00
Natalie L 3aa7b8b447 Gifting modal design (#14124)
* update selectUserModal.vue

* more updates to selectUserModal.vue, typo fix in subscriber.json

* remove exact sizing for selectUserModal.vue

* update to size for selectUserModal.vue

* added sendGiftModal.vue file

* updates to selectUser & sendGift modals

* making the modals go & position cursor

* working working working

* added a return to method

* avatar display & placeholder profile.name and username

* subscription-options added

* added menu row & started on gem options

* Added selectPage function, have not tested.

* updated habitica-images

* state changes

* bringing in gem counter

* arranging elements

* state changes, gem input boxes

* styling sendGiftModal.vue

* more sendGiftModal.vue styling and new close.svg icon

* more styling!

* and more styling of send own gems part of page

* images update

* more styling of own gems & some attempts to adjust :class on the menu

* styling styling styling

* replace +/- svg, styling

* styling, mostly

* new SVGs

* stylin'

* reverting svg changes

* no more stylin'

* finally got the +/- icons to show up...but they're the wrong color

* solved svg icon color problem! :)

* habitica-images

* working on sendGift part of button

* trying to make it do math, failing

* more attempts at math

* +/- buttons work on gem pages & cost calculation on buyGems

* trying to get hover colors working on +/- svgs

* formatted dollar amount as currency

* css/html for subscription-options & payments-buttons simplified

* swag at payments-buttons parameter (not tested)

* send gems from own balance works!

* working on starting page

* increment gem amount limited to maxGems and not < 0

* uncommented onHide()

* got bg color on sub options to work! yay!

* payment buttons!

* making g1g1 look good

* position modal on page properly & code clean-up

* Changes as requested!

* small color update

* fixed ternary function

* chore(html): indentation and comments

* fix(fn): correct catch for under-0

* chore(json): whitespace

* update gem styling; add linebreak to notifications.vue bc linter

* updating subscriptionOptions

* snackbar css fix

* reverting commit e16c12f

* removing merge conflict markers

* just a little comment

* fixed some navigation, clear input field on selectPage, cleaned up code; another try at subscriptionOption.vue

* merge upstream/develop

* update selectPage() to disable Gems menu items when on 'ownGems' or 'buyGems' states

* working on subscriptionOptions.vue logic

* fix(script): changed props & added updateSubscriptionData()

* fix(script): forgot to call updateSubscriptionData()

* fix(scripts): corrected :userReceivingGift on sendGiftModal.vue

* fix(scripts): correct props userReceivingGift to an Object

* fix(scripts): corrected v-if & revised props

* fix(style/html/whitespace): updated css for close.svg and added missing </div>

* style(radio-buttons): updated focus states and added hover states

* style(radio-buttons): refined focus and hover states

* fix(function): changed buyGemsLink to buyGems; still working on menu

* style(radio buttons): ensured consistent display of radio buttons through-out site; still struggling with hover states

* style(radio buttons): updated focus/active/hover to match design & removed unnecessary code

* fix: set default subscription option to 1 month

* fix(function): add default amounts to gem states when modal selected from user profile

* fix(build): use develop package json

* fix: SCSS commenting & abstracted setGemsDefault()

* fix(packages): revert to develop

* fix: remove unnecessary console.log statement

Co-authored-by: SabreCat <sabe@habitica.com>
2022-07-13 14:17:28 -05:00
dependabot[bot] 94b9bb1036 build(deps): bump image-size from 1.0.1 to 1.0.2 (#14123)
Bumps [image-size](https://github.com/image-size/image-size) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/image-size/image-size/releases)
- [Commits](https://github.com/image-size/image-size/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: image-size
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-13 12:11:38 -04:00
dependabot[bot] a2f169ab76 build(deps): bump core-js from 3.23.3 to 3.23.4 in /website/client (#14114)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.3 to 3.23.4.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.23.3...v3.23.4)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-13 12:10:45 -04:00
SabreCat 6d345740ff Merge branch 'release' into develop 2022-07-12 10:06:07 -05:00
SabreCat 294cc63fef 4.236.1 2022-07-12 10:05:09 -05:00
SabreCat 9a879d566e fix(content): correct availability range for Seafoam 2022-07-12 10:05:02 -05:00
Scott Coffrin 8ecb0f45b5 Modal responsive improvements (#14087)
* stack profile actions on smaller screens

* stack avatar and stats for even smaller screens

* remove unnecessary classes to keep profile nav on the same line

* adjust media query width

* refactor stats removing unnecessary classes and simplifying with less elements and relying more on flexbox

* adjust breakpoints for modal vs pofile page

* more margin for avatar

* handle allocation on middle size more gracefully

Co-authored-by: scoffrin <scoffrin@indeed.com>
2022-07-08 15:49:22 -05:00
darkarchana b04df06a37 Fix no response when password changed (#14054)
* Fix no response when password changed

* Update website/client/src/components/settings/site.vue

update the spacing in website/client/src/components/settings/site.vue

Co-authored-by: Panu Valtanen <p4nu@users.noreply.github.com>

* Update website/client/src/components/settings/site.vue

change to single quote

* Fix success change password response using internationalization

* fix(i18n): remove translations other than US English
Partial revert of #1a198677bd

Co-authored-by: Panu Valtanen <p4nu@users.noreply.github.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2022-07-08 15:42:18 -05:00
bbqben 706cffa71d 🐛 Update fetch language call to not retrieve from cache (#14099)
Co-authored-by: Ben Tran <bentran@Bens-Intel-MacBook-Pro.local>
2022-07-06 15:32:07 -05:00
dependabot[bot] 00b8f4fef5 build(deps): bump moment from 2.29.3 to 2.29.4 (#14111)
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-06 15:28:28 -05:00
dependabot[bot] 5ea675b8a5 build(deps): bump async from 2.6.3 to 2.6.4 in /website/client (#14109)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-06 15:28:12 -05:00
dependabot[bot] 78f0c71387 build(deps): bump moment from 2.29.3 to 2.29.4 in /website/client (#14112)
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-06 15:28:01 -05:00
SabreCat e674ef4035 Merge branch 'release' into develop 2022-07-06 14:34:24 -05:00
SabreCat 1655e2e03a 4.236.0 2022-07-06 14:32:58 -05:00
Natalie L 0d444a9d6a chore(content): prebuild July Enchanted Armoire and Backgrounds (#14108) 2022-07-06 14:30:40 -05:00
SabreCat 7b067de4b9 chore(analytics): add tracking for task mirroring preference 2022-07-05 15:10:36 -05:00
dependabot[bot] de331f5e76 build(deps-dev): bump @babel/plugin-proposal-optional-chaining (#14105)
Bumps [@babel/plugin-proposal-optional-chaining](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-optional-chaining) from 7.17.12 to 7.18.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.6/packages/babel-plugin-proposal-optional-chaining)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-optional-chaining"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-05 16:06:03 -04:00
dependabot[bot] c48043ec90 build(deps): bump @vue/cli-plugin-eslint in /website/client (#14106)
Bumps [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-eslint) from 4.5.18 to 4.5.19.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.5.19/packages/@vue/cli-plugin-eslint)

---
updated-dependencies:
- dependency-name: "@vue/cli-plugin-eslint"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-05 16:03:41 -04:00
dependabot[bot] ac4d148170 build(deps): bump winston from 3.8.0 to 3.8.1 (#14103)
Bumps [winston](https://github.com/winstonjs/winston) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.8.0...v3.8.1)

---
updated-dependencies:
- dependency-name: winston
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-05 16:01:03 -04:00
dependabot[bot] ca49e995be build(deps): bump @babel/preset-env from 7.18.2 to 7.18.6 (#14098)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.18.2 to 7.18.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.6/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-05 15:43:56 -04:00
SabreCat 9453b1269e Merge branch 'develop' into sabrecat/teams-rebase 2022-07-05 14:34:53 -05:00
SabreCat d47a867149 Merge branch 'release' into develop 2022-07-05 14:34:30 -05:00
SabreCat f4d9c6271b 4.235.1 2022-07-05 14:34:17 -05:00
Natalie L 0e458683fd chore(content): add splashySkins for Summer Gala event (#14107)
* chore(content): add splashySkins for Summer Gala event

* fix(content): use date string, not Boolean, for range start

Co-authored-by: SabreCat <sabe@habitica.com>
2022-07-05 14:33:50 -05:00
Alys 76ab93f501 add deilann to moderators in Tavern and Community Guidelines
Note that his picture is still needed in the Community Guidelines
and when that's done, the "(not yet pictured)" text should be removed
2022-07-02 14:04:09 +10:00
Alys 5d4600f5c7 add banned words - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2022-07-01 20:55:34 +10:00
SabreCat cf536a82f8 fix(teams): more style updates 2022-06-30 16:55:47 -05:00
dependabot[bot] 13c4a726c7 build(deps): bump @babel/core from 7.18.5 to 7.18.6 (#14097)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.5 to 7.18.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.6/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 13:45:15 -04:00
dependabot[bot] 43122805fb build(deps): bump @babel/register from 7.17.7 to 7.18.6 (#14096)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.17.7 to 7.18.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.6/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 13:44:55 -04:00
dependabot[bot] 1262d8f36e build(deps): bump core-js from 3.23.1 to 3.23.3 in /website/client (#14094)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.1 to 3.23.3.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.23.1...v3.23.3)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 13:42:41 -04:00
dependabot[bot] 2b1635ff62 build(deps): bump winston from 3.7.2 to 3.8.0 (#14093)
Bumps [winston](https://github.com/winstonjs/winston) from 3.7.2 to 3.8.0.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.7.2...v3.8.0)

---
updated-dependencies:
- dependency-name: winston
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 13:42:19 -04:00
dependabot[bot] e1664d2f87 build(deps): bump amplitude-js from 8.18.4 to 8.18.5 in /website/client (#14089)
Bumps [amplitude-js](https://github.com/amplitude/amplitude-javascript) from 8.18.4 to 8.18.5.
- [Release notes](https://github.com/amplitude/amplitude-javascript/releases)
- [Changelog](https://github.com/amplitude/Amplitude-JavaScript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amplitude/amplitude-javascript/compare/v8.18.4...v8.18.5)

---
updated-dependencies:
- dependency-name: amplitude-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 13:39:42 -04:00
SabreCat 5967e4356c fix(teams): style updates 2022-06-29 15:52:54 -05:00
SabreCat 6ebfa976fe Merge branch 'develop' into sabrecat/teams-rebase 2022-06-29 14:30:25 -05:00
SabreCat 6975b6061b Merge branch 'release' into develop 2022-06-29 14:04:39 -05:00
SabreCat ddd5f20609 fix(teams): don't complain about move route when not moving 2022-06-29 09:06:08 -05:00
SabreCat a3f61306d3 feat(teams): user preference toggle for mirroring 2022-06-28 16:18:24 -05:00
SabreCat 712b85ce84 fix(teams): complete task sorting 2022-06-24 16:43:41 -05:00
Natalie L 9680c94087 fix(strings): removed extra word in headSpecialSummer2022WarriorNotes (#14088)
* fix(string): questVice1Notes html changed to a mobile-device friendly format

* fix(strings): updated limited.json with "dateEnd" & "monthYYYY" months & put in chronological order

* fix(string): remove extra word from headSpecialSummer2022WarriorNotes

* fix(string): corrected armorSpecialSummer2022MageNotes
2022-06-24 15:43:38 -05:00
SabreCat 9142588ba7 fix(tasks): better tasksOrder maintenance 2022-06-23 16:44:21 -05:00
Weblate 76de241675 Added translation using Weblate (Cebuano)
Added translation using Weblate (Cebuano)

Added translation using Weblate (Cebuano)

Added translation using Weblate (Cebuano)

Merge branch 'origin/develop' into Weblate.

Translated using Weblate (Portuguese)

Currently translated at 83.9% (115 of 137 strings)

Translated using Weblate (Portuguese)

Currently translated at 83.9% (115 of 137 strings)

Translated using Weblate (Portuguese)

Currently translated at 83.9% (115 of 137 strings)

Translated using Weblate (Greek)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2607 of 2607 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2607 of 2607 strings)

Translated using Weblate (Filipino)

Currently translated at 43.8% (1139 of 2597 strings)

Translated using Weblate (Filipino)

Currently translated at 43.7% (1137 of 2597 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2607 of 2607 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (704 of 704 strings)

Translated using Weblate (Dutch)

Currently translated at 90.6% (2363 of 2607 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (704 of 704 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (135 of 137 strings)

Translated using Weblate (French)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (German)

Currently translated at 100.0% (755 of 755 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Lucifer <selmanreyhan@gmail.com>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Martim Pinto Paiva <pintopaivam@gmail.com>
Co-authored-by: Nathan Monteiro <nathanspeeds1@outlook.com>
Co-authored-by: Panagiotis Zachos <panzaxos@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: SunshineRain <suusykraft@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/el/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Gear
Translation: Habitica/Overview
Translation: Habitica/Questscontent
2022-06-22 21:18:17 +02:00
SabreCat 3ba6b4a209 feat(language): initialize Cebuano for translation 2022-06-22 14:04:51 -05:00
SabreCat b657172a2b Merge branch 'develop' into sabrecat/teams-rebase 2022-06-21 13:50:50 -05:00
SabreCat b101d43e62 Merge branch 'release' into develop 2022-06-21 13:50:35 -05:00
dependabot[bot] c0e8d80966 build(deps): bump @vue/cli-plugin-eslint in /website/client (#14084)
Bumps [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-eslint) from 4.5.17 to 4.5.18.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.5.18/packages/@vue/cli-plugin-eslint)

---
updated-dependencies:
- dependency-name: "@vue/cli-plugin-eslint"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-21 13:02:34 -04:00
dependabot[bot] afacd497d7 build(deps): bump core-js from 3.22.8 to 3.23.1 in /website/client (#14082)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.22.8 to 3.23.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.22.8...v3.23.1)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-21 13:02:07 -04:00
SabreCat b790b87ca8 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-21 09:34:17 -05:00
SabreCat dc744de4a9 Merge branch 'release' into develop 2022-06-21 09:33:35 -05:00
dependabot[bot] f44bebb573 build(deps): bump jpeg-js from 0.4.3 to 0.4.4 (#14071)
Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-17 13:26:08 -04:00
SabreCat 9d3059fc30 fix(teams): copy in assignee username during migration 2022-06-16 16:04:55 -05:00
dependabot[bot] 8ccf701aec build(deps): bump @storybook/addons in /website/client (#14066)
Bumps [@storybook/addons](https://github.com/storybookjs/storybook/tree/HEAD/lib/addons) from 6.5.8 to 6.5.9.
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/v6.5.9/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v6.5.9/lib/addons)

---
updated-dependencies:
- dependency-name: "@storybook/addons"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-16 16:18:59 -04:00
SabreCat 647371accc Merge branch 'develop' into sabrecat/teams-rebase 2022-06-16 14:11:47 -05:00
SabreCat 8b084e627e WIP(teams): show open tasks on user view 2022-06-16 13:25:09 -05:00
SabreCat 4ac1a3e717 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-14 14:46:50 -05:00
SabreCat a0177fa44d WIP(teams): display assigned tasks on user's personal board 2022-06-13 16:53:29 -05:00
SabreCat 9fec111c4d Merge branch 'develop' into sabrecat/teams-rebase 2022-06-06 15:15:16 -05:00
SabreCat a559c1add8 refactor(tasks): get rid of behind-the-scenes task cloning 2022-06-03 16:40:09 -05:00
SabreCat 5868849034 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-03 16:04:35 -05:00
SabreCat c98c7ab26c Merge branch 'develop' into sabrecat/teams-rebase 2022-05-31 15:51:16 -05:00
SabreCat 1ef7924ba5 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-24 09:39:01 -05:00
SabreCat 7651e6a540 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-19 14:30:29 -05:00
SabreCat bb20c44fde fix(needs-work): don't show Gold/Experience depletion for manager 2022-05-19 14:28:35 -05:00
SabreCat eb99ca0411 fix(cron): reset completions even if Daily wasn't due 2022-05-17 14:02:18 -05:00
SabreCat 9c24d43a13 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-17 09:34:51 -05:00
SabreCat bf9a7ea7d9 fix(teams): take user to relevant group from task assignment click 2022-05-13 14:26:14 -05:00
SabreCat ca1dbd2fc4 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-12 13:58:15 -05:00
SabreCat 04107ed6d3 fix(tasks): 12px padding, not 16 2022-05-10 15:36:43 -05:00
SabreCat 209b7bd1aa Merge branch 'develop' into sabrecat/teams-rebase 2022-05-10 13:58:14 -05:00
SabreCat db354875ee fix(teams): adjust task title spacing, don't damage user for team Daily 2022-05-06 14:23:09 -05:00
SabreCat 40af14b061 fix(multiassign): don't allow nonmanagers to uncheck from footer 2022-05-05 15:16:30 -05:00
SabreCat 1d048e0c35 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-05 13:56:36 -05:00
SabreCat 6a5f467a35 fix(teams): hover states, missing snackbars 2022-05-04 17:02:09 -05:00
SabreCat 86b0d6d86c fix(cron): handle when leader not found 2022-05-04 14:10:57 -05:00
SabreCat 565d33f6a7 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-03 15:53:11 -05:00
SabreCat 588e5dd487 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-29 14:42:08 -05:00
SabreCat d8fbf9420e fix(teams): delete assignedUsers on open tasks 2022-04-28 15:48:40 -05:00
SabreCat edb606814c feat(teams): beta testing banner 2022-04-22 15:47:55 -05:00
SabreCat 32823e3760 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-21 14:10:27 -05:00
SabreCat 48f5ffc997 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-15 14:26:19 -05:00
SabreCat 90375e3bc4 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-14 15:36:26 -05:00
SabreCat c4a92ba384 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-12 14:48:45 -05:00
SabreCat 8f7e5d544e Merge branch 'develop' into sabrecat/teams-rebase 2022-04-07 14:55:16 -05:00
SabreCat 40113b0458 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-05 13:56:25 -05:00
SabreCat 48be0a38bf Merge branch 'develop' into sabrecat/teams-rebase 2022-03-31 14:49:35 -05:00
SabreCat 459b327e2d feat(nav): clicking "Group" goes to first group 2022-03-30 15:53:48 -05:00
SabreCat d3fde93762 fix(cron): don't adjust task decay by "assignments" for open Daily 2022-03-30 13:06:50 -05:00
SabreCat aa81c330af Merge branch 'develop' into sabrecat/teams-rebase 2022-03-30 11:35:33 -05:00
SabreCat 7283d112f4 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-29 14:10:18 -05:00
SabreCat f1b0aa2e7c Merge branch 'develop' into sabrecat/teams-rebase 2022-03-28 16:34:31 -05:00
SabreCat 354f3578a2 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-23 20:23:29 -05:00
SabreCat c5ef803458 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-22 16:29:47 -05:00
SabreCat 40801c0d32 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-15 15:05:25 -05:00
SabreCat e9ca17bbd8 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-14 14:33:36 -05:00
SabreCat 5a638ab4b8 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-09 16:37:31 -06:00
SabreCat 7fa4e6f791 fix(teams): don't show status footer for Habits or Rewards 2022-03-09 11:24:19 -06:00
SabreCat 61be42bf05 WIP(teams): data migration draft 2022-03-04 18:17:43 -06:00
SabreCat 9d4bf22720 fix(teams): Close button and padding fix 2022-03-04 15:15:50 -06:00
SabreCat 24349bed0a fix(teams): display and logic adjustments 2022-03-01 16:18:57 -06:00
SabreCat 17b93322aa fix(teams): reload completed To Do 2022-02-28 16:28:54 -06:00
SabreCat ef6d92e7af Merge branch 'develop' into sabrecat/teams-rebase 2022-02-28 09:50:07 -06:00
SabreCat 35ed158dd9 WIP(teams): updated completion states
and fixed an issue with cron saving
2022-02-24 16:23:46 -06:00
SabreCat 31cbcf53a2 Merge branch 'develop' into sabrecat/teams-rebase 2022-02-24 14:54:34 -06:00
SabreCat 3a2fd28199 WIP(tasks): start task refresh process 2022-02-22 16:50:35 -06:00
SabreCat 1473408752 Merge branch 'develop' into sabrecat/teams-rebase 2022-02-22 12:18:59 -06:00
SabreCat 53babfb9fe WIP(teams): snakey checkmark 2022-02-18 17:41:32 -06:00
SabreCat 64694c3a29 WIP(teams): partial single-assign Dailies styling 2022-02-17 22:00:45 -06:00
SabreCat 1bd2ec0463 fix(branch): reset package files to develop 2022-02-17 16:07:53 -06:00
SabreCat 4e9625454c WIP(teams): new single assign footer states for To Do's 2022-02-17 15:52:59 -06:00
SabreCat a58dd35fbe WIP(teams): tiny checkmarks 2022-02-17 15:52:59 -06:00
SabreCat 54088f5374 WIP(teams): fixes from demo session 2022-02-17 15:52:59 -06:00
SabreCat 9eebcf9b16 fix(deps): bad babel-eslint version? 2022-02-17 15:52:58 -06:00
SabreCat 44722a0d4c chore(deps): update package locks 2022-02-17 15:52:58 -06:00
SabreCat 7eae0a83f9 4.221.2 2022-02-17 15:52:40 -06:00
SabreCat 294b94206f fix(tz): remove moment-timezone completely 2022-02-17 15:51:55 -06:00
SabreCat 7e73c336dd WIP(teams): stylish and functional multi assign checkboxes 2022-02-17 15:51:28 -06:00
SabreCat 82d3545c08 WIP(teams): various fixes 2022-02-17 15:51:28 -06:00
SabreCat e312ea943f WIP(teams): partial style implementation for status rows 2022-02-17 15:51:27 -06:00
SabreCat a4a1595ec7 WIP(teams): very janky multi status 2022-02-17 15:51:27 -06:00
SabreCat 4b07e3a116 WIP(teams): start of multi state implementation 2022-02-17 15:51:27 -06:00
SabreCat 13e645fa4b fix(teams): correct some unassignment bugs 2022-02-17 15:51:27 -06:00
SabreCat 0d876472a3 WIP(teams): fix initial assignment sync, add Daily handling 2022-02-17 15:51:26 -06:00
SabreCat 9e527f4f35 WIP(teams): can do To Do's 2022-02-17 15:51:09 -06:00
SabreCat eaa5f821a4 WIP(multi-assign): functioning multi 2022-02-17 15:50:44 -06:00
SabreCat a495db8480 WIP(assignment): change to object style
to do--let assignment API accept an array
2022-02-17 15:50:42 -06:00
SabreCat fa99458ca4 WIP(multiassign): resume shared completion implementation 2022-02-17 15:50:23 -06:00
Sabe Jones 1f81e1971b fix(cron): remove now redundant logic 2022-02-17 15:49:56 -06:00
Sabe Jones 45fc2b62e3 fix(cron): process team board tasks assigned to the user 2022-02-17 15:49:56 -06:00
Sabe Jones f843564444 fix(storybook): temporarily disable story 2022-02-17 15:49:55 -06:00
Sabe Jones fb216fba8e fix(cron): reset checklists as needed 2022-02-17 15:49:54 -06:00
Sabe Jones 603cc93957 fix(ui): further cursor tweaks for Teams 2022-02-17 15:49:54 -06:00
Sabe Jones 63e0875f32 fix(teams): allow ticking team checklists 2022-02-17 15:49:34 -06:00
Sabe Jones 029e41472f fix(teams): filter further to hide team tasks, don't override 2022-02-17 15:49:19 -06:00
Sabe Jones 1752c08fd9 fix(teams): allow managers to reorder tasks 2022-02-17 15:49:19 -06:00
Sabe Jones 395d9e7650 fix(teams): maybe goodish style? 2022-02-17 15:49:18 -06:00
Sabe Jones 61d396204f fix(teams): more layout tweakage 2022-02-17 15:49:17 -06:00
Sabe Jones f233c511cc fix(teams): layout issues, error, change timezone format 2022-02-17 15:49:16 -06:00
Sabe Jones 0806391ab8 feat(teams): Day Start and adjust uncheck wording 2022-02-17 15:48:55 -06:00
Sabe Jones dcaba7f186 fix(teams): update beta banner wording 2022-02-17 15:47:55 -06:00
Sabe Jones 59dc97b75f WIP(teams): fixes, beta banner 2022-02-17 15:47:37 -06:00
Sabe Jones 85a9ea726c WIP(teams): draft of server literal-actual-cron script 2022-02-17 15:47:23 -06:00
Sabe Jones d53813adc7 WIP(teams): add some analytics, remove extraneous logic 2022-02-17 15:47:02 -06:00
Sabe Jones 221dd7a81e fix(tasks): more lock icon revision, no error on manager uncheck 2022-02-17 15:46:45 -06:00
Sabe Jones 99bf6349e1 fix(assignment): update single select dropdown style 2022-02-17 15:46:44 -06:00
Sabe Jones 801b902bb8 fix(tasks): manager lock icon, coerce task value to Number 2022-02-17 15:46:43 -06:00
Sabe Jones 072b09e030 feat(teams): quick add
Also fixes issue with lock icons
Also hides task copies from personal task board
2022-02-17 15:46:42 -06:00
Sabe Jones a5680836bd feat(teams): new disapproval workflow, managers can uncheck tasks 2022-02-17 15:46:23 -06:00
Sabe Jones bb9ba61d12 feat(teams): show team name 2022-02-17 15:44:58 -06:00
Sabe Jones a88f97831a fix(teams): update single select style and correct create/edit issue 2022-02-17 15:44:58 -06:00
Sabe Jones ae0528e5cd WIP(teams): fix initially unassigned task, add completedBy data 2022-02-17 15:44:57 -06:00
Sabe Jones 74345adf6b fix(teams): make selectSingle workflows functional 2022-02-17 15:44:56 -06:00
negue 7dbee4caed clone selectMulti to selectSingle for assignedMember 2022-02-17 15:44:55 -06:00
Sabe Jones f4feb09fbc fix(cron): actually process cron for assigned tasks 2022-02-17 15:44:54 -06:00
Sabe Jones 6cddb3bf82 WIP(teams): more partial fixing 2022-02-17 15:44:36 -06:00
Sabe Jones 248e1c6fe9 WIP(teams): reimplement open tasking 2022-02-17 15:43:28 -06:00
Sabe Jones 6e39c79cff WIP(teams): simplify task footers 2022-02-17 15:41:07 -06:00
Sabe Jones 5bf4e18ce8 WIP(teams): don't damage leader for incomplete team Dailies 2022-02-17 15:41:06 -06:00
Sabe Jones cab4a2a8fa WIP(teams): begin simplification 2022-02-17 15:41:05 -06:00
427 changed files with 13238 additions and 9389 deletions
+1
View File
@@ -2,6 +2,7 @@
"name": "Habitica V3 API Documentation",
"title": "Habitica",
"url": "https://habitica.com",
"version": "3.0.0",
"sampleUrl": null,
"header": {
"title": "Introduction",
+1
View File
@@ -1,4 +1,5 @@
{
"ACCOUNT_MIN_CHAT_AGE": "0",
"ADMIN_EMAIL": "you@example.com",
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
"AMAZON_PAYMENTS_MODE": "sandbox",
+71
View File
@@ -0,0 +1,71 @@
import filter from 'lodash/filter';
import find from 'lodash/find';
import isArray from 'lodash/isArray';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
import * as Tasks from '../../website/server/models/task';
async function updateTeamTasks (team) {
const toSave = [];
const teamTasks = await Tasks.Task.find({
'group.id': team._id,
}).exec();
const teamBoardTasks = filter(teamTasks, task => !task.userId);
const teamUserTasks = filter(teamTasks, task => task.userId);
for (const boardTask of teamBoardTasks) {
if (isArray(boardTask.group.assignedUsers)) {
boardTask.group.approval = undefined;
boardTask.group.assignedDate = undefined;
boardTask.group.assigningUsername = undefined;
boardTask.group.sharedCompletion = undefined;
for (const assignedUserId of boardTask.group.assignedUsers) {
const assignedUser = await User.findById(assignedUserId, 'auth'); // eslint-disable-line no-await-in-loop
const userTask = find(teamUserTasks, task => task.userId === assignedUserId
&& task.group.taskId === boardTask._id);
if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {};
if (userTask && assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: userTask.group.assignedDate,
assignedUsername: assignedUser.auth.local.username,
assigningUsername: userTask.group.assigningUsername,
completed: userTask.completed || false,
completedDate: userTask.dateCompleted,
};
} else if (assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: new Date(),
assignedUsername: assignedUser.auth.local.username,
assigningUsername: null,
completed: false,
completedDate: null,
};
} else {
const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId);
boardTask.group.assignedUsers.splice(taskIndex, 1);
}
if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id));
}
boardTask.markModified('group');
toSave.push(boardTask.save());
}
}
return Promise.all(toSave);
}
export default async function processTeams () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const taskPromises = activeTeams.map(updateTeamTasks);
return Promise.all(taskPromises);
}
+1056 -1118
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,19 +1,19 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.235.0",
"version": "4.242.1",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"@babel/register": "^7.17.7",
"@babel/core": "^7.18.13",
"@babel/preset-env": "^7.18.10",
"@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^5.1.6",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.51.1",
"apidoc": "^0.52.0",
"apple-auth": "^1.0.7",
"bcrypt": "^5.0.1",
"body-parser": "^1.20.0",
@@ -39,7 +39,7 @@
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
"image-size": "^1.0.1",
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^4.0.2",
"jsonwebtoken": "^8.5.1",
@@ -47,7 +47,7 @@
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.3",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
@@ -74,7 +74,7 @@
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.7.2",
"winston": "^3.8.1",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
},
@@ -121,7 +121,7 @@
"mocha": "^5.1.1",
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.6",
"run-rs": "^0.7.7",
"sinon": "^13.0.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
+105
View File
@@ -0,0 +1,105 @@
import forEach from 'lodash/forEach';
import { model as Group } from '../website/server/models/group';
import { model as User } from '../website/server/models/user';
import * as Tasks from '../website/server/models/task';
import { daysSince, shouldDo } from '../website/common/script/cron';
const TASK_VALUE_CHANGE_FACTOR = 0.9747;
const MIN_TASK_VALUE = -47.27;
async function updateTeamTasks (team) {
const toSave = [];
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
if (!teamLeader) { // why would this happen?
teamLeader = {
preferences: { }, // when options are sanitized this becomes CDS 0 at UTC
};
}
if (
!team.cron || !team.cron.lastProcessed
|| daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0
) {
const tasks = await Tasks.Task.find({
'group.id': team._id,
userId: { $exists: false },
$or: [
{ type: 'todo', completed: false },
{ type: { $in: ['habit', 'daily'] } },
],
}).exec();
const tasksByType = {
habits: [], dailys: [], todos: [], rewards: [],
};
forEach(tasks, task => tasksByType[`${task.type}s`].push(task));
forEach(tasksByType.habits, habit => {
if (!(habit.up && habit.down) && habit.value !== 0) {
habit.value *= 0.5;
if (Math.abs(habit.value) < 0.1) habit.value = 0;
toSave.push(habit.save());
}
});
forEach(tasksByType.todos, todo => {
if (!todo.completed) {
const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value;
todo.value -= delta;
if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE;
toSave.push(todo.save());
}
});
forEach(tasksByType.dailys, daily => {
let processChecklist = false;
let assignments = 0;
let completions = 0;
for (const assignedUser in daily.group.assignedUsersDetail) {
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsersDetail, assignedUser)) {
assignments += 1;
if (daily.group.assignedUsersDetail[assignedUser].completed) {
completions += 1;
daily.group.assignedUsersDetail[assignedUser].completed = false;
}
}
}
if (completions > 0) daily.markModified('group.assignedUsersDetail');
if (daily.completed) {
processChecklist = true;
daily.completed = false;
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
processChecklist = true;
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
if (assignments > 0) {
daily.value -= ((completions / assignments) * delta);
}
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
}
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);
if (processChecklist && daily.checklist.length > 0) {
daily.checklist.forEach(i => { i.completed = false; });
}
toSave.push(daily.save());
});
if (!team.cron) team.cron = {};
team.cron.lastProcessed = new Date();
toSave.push(team.save());
}
return Promise.all(toSave);
}
export default async function processTeamsCron () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const cronPromises = activeTeams.map(updateTeamTasks);
return Promise.all(cronPromises);
}
+11 -210
View File
@@ -1,12 +1,10 @@
import { each, find, findIndex } from 'lodash';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { each, findIndex } from 'lodash';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
describe('Group Task Methods', () => {
let guild; let leader; let challenge; let
task;
let guild; let leader; let task;
const tasksToTest = {
habit: {
text: 'test habit',
@@ -31,10 +29,6 @@ describe('Group Task Methods', () => {
},
};
function findLinkedTask (updatedLeadersTask) {
return updatedLeadersTask.group.taskId === task._id;
}
beforeEach(async () => {
guild = new Group({
name: 'test party',
@@ -47,19 +41,9 @@ describe('Group Task Methods', () => {
guild.leader = leader._id;
challenge = new Challenge({
name: 'Test Challenge',
shortName: 'Test',
leader: leader._id,
group: guild._id,
});
leader.challenges = [challenge._id];
await Promise.all([
guild.save(),
leader.save(),
challenge.save(),
]);
});
@@ -78,7 +62,15 @@ describe('Group Task Methods', () => {
});
it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, leader);
await guild.syncTask(task, [leader], leader);
const updatedTask = await Tasks.Task.findOne({ _id: task._id });
expect(updatedTask.group.assignedUsers).to.contain(leader._id);
expect(updatedTask.group.assignedUsersDetail[leader._id]).to.exist;
});
it('creates tags for a user when task is synced', async () => {
await guild.syncTask(task, [leader], leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const tagIndex = findIndex(updatedLeader.tags, { id: guild._id });
@@ -88,197 +80,6 @@ describe('Group Task Methods', () => {
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
});
it('syncs updated info for assigned task to a user', async () => {
await guild.syncTask(task, leader);
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
});
it('syncs checklist items to an assigned user', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
if (task.type !== 'daily' && task.type !== 'todo') return;
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
});
describe('syncs updated info', async () => {
let newMember;
beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, { newCheckListItem });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, { updateCheckListItems: [task.checklist[0]] });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, { removedCheckListItemId: task.checklist[0].id });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
});
it('removes assigned tasks when master task is deleted', async () => {
await guild.syncTask(task, leader);
await guild.removeTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
await guild.syncTask(task, leader);
await guild.unlinkTask(task, leader, 'remove-all');
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and keeps group tasks for a user when keep-all is specified', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({ _id: leader._id });
let updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
await guild.unlinkTask(task, leader, 'keep-all');
updatedLeader = await User.findOne({ _id: leader._id });
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const updatedSyncedTask = find(
updatedLeadersTasks,
updatedLeadersTask => updatedLeadersTask._id === syncedTask._id,
);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(updatedSyncedTask).to.exist;
expect(updatedSyncedTask.group._id).to.be.undefined;
});
});
});
});
+13 -6
View File
@@ -246,13 +246,23 @@ describe('Task Model', () => {
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
});
it('scopes alias lookup to user', async () => {
it('scopes alias lookup to user when querying aliases only', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
alias: { $in: [taskWithAlias.alias] },
userId: user._id,
});
});
it('scopes alias lookup to user when querying aliases and IDs', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias, secondTask._id], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
$or: [
{ _id: { $in: [] } },
{ _id: { $in: [secondTask._id] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
userId: user._id,
@@ -270,10 +280,7 @@ describe('Task Model', () => {
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
$or: [
{ _id: { $in: [] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
alias: { $in: [taskWithAlias.alias] },
userId: user._id,
foo: 'bar',
});
@@ -4,6 +4,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('POST /challenges', () => {
it('returns error when group is empty', async () => {
@@ -60,6 +61,22 @@ describe('POST /challenges', () => {
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({
members: 1,
});
await expect(user.post('/challenges', {
group: group._id,
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
@@ -4,6 +4,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('PUT /challenges/:challengeId', () => {
let privateGuild; let user; let nonMember; let challenge; let
@@ -91,4 +92,15 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.name).to.equal('New Challenge Name');
expect(res.description).to.equal('New challenge description.');
});
it('return error when challenge summary is greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
await expect(user.put(`/challenges/${challenge._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
@@ -15,6 +15,10 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
type: 'guild',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
groupWithChat = group;
@@ -117,7 +117,9 @@ describe('POST /chat/:chatId/flag', () => {
});
it('Flags a chat when the author\'s account was deleted', async () => {
const deletedUser = await generateUser();
const deletedUser = await generateUser({
'auth.timestamps.created': new Date('2022-01-01'),
});
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
password: 'password',
@@ -18,11 +18,16 @@ describe('POST /chat/:chatId/like', () => {
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
user = groupLeader;
groupWithChat = group;
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('Returns an error when chat message is not found', async () => {
+41 -5
View File
@@ -38,10 +38,15 @@ describe('POST /chat', () => {
members: 2,
});
user = groupLeader;
await user.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); // prevent tests accidentally throwing messageGroupChatSpam
await user.update({
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
member = members[0]; // eslint-disable-line prefer-destructuring
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('Returns an error when no message is provided', async () => {
@@ -104,7 +109,10 @@ describe('POST /chat', () => {
});
const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
await privateGuildMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -122,7 +130,10 @@ describe('POST /chat', () => {
});
const privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
await privatePartyMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -183,7 +194,10 @@ describe('POST /chat', () => {
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -202,7 +216,10 @@ describe('POST /chat', () => {
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -312,6 +329,7 @@ describe('POST /chat', () => {
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
@@ -330,6 +348,7 @@ describe('POST /chat', () => {
// Update the bannedWordsAllowed property for the group
group.update({ bannedWordsAllowed: true });
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
@@ -345,6 +364,7 @@ describe('POST /chat', () => {
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
@@ -411,6 +431,7 @@ describe('POST /chat', () => {
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage });
@@ -430,6 +451,16 @@ describe('POST /chat', () => {
});
});
it('errors when user account is too young', async () => {
const brandNewUser = await generateUser();
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('chatTemporarilyUnavailable'),
});
});
it('creates a chat', async () => {
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
@@ -492,6 +523,7 @@ describe('POST /chat', () => {
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
@@ -517,6 +549,7 @@ describe('POST /chat', () => {
};
const backer = await generateUser({
backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
@@ -587,6 +620,9 @@ describe('POST /chat', () => {
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
},
});
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -15,6 +15,10 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
guild = group;
@@ -51,6 +55,9 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
},
});
party = group;
@@ -18,6 +18,10 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
type: 'guild',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
groupWithChat = group;
@@ -65,6 +69,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
privateMessage = privateMessage.message;
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -11,10 +10,6 @@ describe('POST /group/:groupId/remove-manager', () => {
const groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
@@ -63,28 +58,4 @@ describe('POST /group/:groupId/remove-manager', () => {
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
const task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
const memberTasks = await nonManager.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(1); // user gets mystery items
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => {
let user;
@@ -71,6 +72,20 @@ describe('POST /group', () => {
expect(updatedGroup.summary).to.eql(summary);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const name = 'Test Group';
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', {
name,
type: 'guild',
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Guilds', () => {
@@ -37,6 +37,7 @@ describe('POST /groups/:groupId/leave', () => {
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
memberCount = group.memberCount;
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
@@ -152,6 +153,10 @@ describe('POST /groups/:groupId/leave', () => {
type: 'guild',
},
invites: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
privateGuild = group;
@@ -153,6 +153,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
},
invites: 1,
members: 2,
leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') },
});
party = group;
@@ -3,6 +3,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('PUT /group', () => {
let leader; let nonLeader; let groupToUpdate; let
@@ -130,4 +131,15 @@ describe('PUT /group', () => {
expect(response.bannedWordsAllowed).to.eql(undefined);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
@@ -25,6 +25,7 @@ describe('Prevent multiple notifications', () => {
for (let i = 0; i < 4; i += 1) {
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) {
await partyMembers[memberIndex].update({ 'auth.timestamps.created': new Date('2022-01-01') }); // eslint-disable-line no-await-in-loop
multipleChatMessages.push(
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
);
@@ -13,6 +13,9 @@ describe('POST /tasks/clearCompletedTodos', () => {
{ 'purchased.plan.customerId': 'group-unlimited' },
);
const challenge = await generateChallenge(user, guild);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post(`/challenges/${challenge._id}/join`);
const initialTodoCount = user.tasksOrder.todos.length;
@@ -33,7 +36,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
const tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 7);
@@ -30,7 +30,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
expect(savedTask.checklist[0].completed).to.equal(true);
});
it('can use a alias to score a checklist item', async () => {
it('can use an alias to score a checklist item', async () => {
const task = await user.post('/tasks/user', {
type: 'daily',
text: 'Daily with checklist',
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
translate as t,
createAndPopulateGroup,
@@ -8,10 +7,6 @@ describe('Groups DELETE /tasks/:id', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -35,8 +30,7 @@ describe('Groups DELETE /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
});
it('deletes a group task', async () => {
@@ -64,81 +58,4 @@ describe('Groups DELETE /tasks/:id', () => {
message: t('messageTaskNotFound'),
});
});
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3); // mystery items
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(3);
expect(member2.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(member2.notifications.length).to.equal(2);
});
it('deletes task from assigned user', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask).to.not.exist;
});
it('deletes task from all assigned users', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask).to.not.exist;
expect(member2SyncedTask).to.not.exist;
});
it('prevents a user from deleting a task they are assigned to', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.del(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDeleteAssignedGroupTasks'),
});
});
it('allows a user to delete a task after leaving a group', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/groups/${guild._id}/leave`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
});
@@ -1,78 +0,0 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
describe('GET /approvals/group/:groupId', () => {
let user; let guild; let member; let addlMember; let task; let syncedTask; let
addlSyncedTask;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
addlMember = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${addlMember._id}`);
const memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
const addlMemberTasks = await addlMember.get('/tasks/user');
addlSyncedTask = find(addlMemberTasks, findAssignedTask);
try {
await member.post(`/tasks/${syncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
try {
await addlMember.post(`/tasks/${addlSyncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
});
it('provides only user\'s own tasks when user is not the group leader', async () => {
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]).to.not.exist;
});
it('allows group leaders to get a list of tasks that need approval', async () => {
const approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
it('allows managers to get a list of tasks that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
});
@@ -1,261 +0,0 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
describe('POST /tasks/:id/approve/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
member2 = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(member2.notifications.length).to.equal(1);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
});
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -8,10 +7,6 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -37,14 +32,15 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
code: 400,
error: 'BadRequest',
message: 'Task not completed by this user.',
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await member.post(`/tasks/${task._id}/score/up`);
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
@@ -54,132 +50,64 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
it('marks a task as needing more work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
// score task to require approval
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = user.profile.name;
const taskText = task.text;
const managerName = user.auth.local.username;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(user._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await user.sync();
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('allows a manager to mark a task as needing work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
// score task to require approval
await member.post(`/tasks/${syncedTask._id}/score/up`);
const initialNotifications = member.notifications.length;
await member.post(`/tasks/${task._id}/score/up`);
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = member2.profile.name;
const taskText = task.text;
const managerName = member2.auth.local.username;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(member2._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await Promise.all([user.sync(), member2.sync()]);
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
expect(member2.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('prevents marking a task as needing work if it was already approved', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
});
@@ -8,10 +8,6 @@ describe('POST /tasks/:id/score/:direction', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -30,209 +26,50 @@ describe('POST /tasks/:id/score/:direction', () => {
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
});
it('prevents user from scoring a task that needs to be approved', async () => {
await user.update({
'preferences.language': 'cs',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const response = await member.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskRequiresApproval'));
});
it('allows a user to score an approved task', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('completes master task when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
it('completes single-assigned task', async () => {
await member.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(true);
expect(sourceTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
it('errors when task has already been completed', async () => {
await member.post(`/tasks/${task._id}/score/up`);
await expect(member.post(`/tasks/${task._id}/score/up`)).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
it('does not complete multi-assigned task when not all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(false);
expect(sourceTask.completed).to.equal(false);
});
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
it('completes multi-assigned task when all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const member2Tasks = await member2.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
await member2.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(true);
expect(sourceTask.completed).to.equal(true);
});
});
@@ -39,7 +39,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when task is not found', async () => {
await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`))
await expect(user.post(`/tasks/${generateUUID()}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -56,7 +56,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
notes: 1976,
});
await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`))
await expect(user.post(`/tasks/${nonGroupTask._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -67,7 +67,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
it('returns error when user is not a member of the group', async () => {
const nonUser = await generateUser();
await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`))
await expect(nonUser.post(`/tasks/${task._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -76,7 +76,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when non leader tries to create a task', async () => {
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
await expect(member2.post(`/tasks/${task._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -84,49 +84,23 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
});
it('allows user to assign themselves (claim)', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
it('sends notifications to group leader and managers when a task is claimed', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member.post(`/tasks/${task._id}/assign/${member._id}`);
await user.sync();
await member2.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(user.notifications.length).to.equal(3); // includes Guild Joined achievement
expect(user.notifications[2].type).to.equal('GROUP_TASK_CLAIMED');
expect(user.notifications[2].data.taskId).to.equal(groupTask[0]._id);
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
expect(member2.notifications[1].data.taskId).to.equal(groupTask[0]._id);
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await member.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
@@ -137,20 +111,27 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(member1SyncedTask).to.exist;
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(groupTask[0].group.assignedUsersDetail[member2._id]).to.exist;
expect(member2SyncedTask).to.exist;
});
@@ -159,13 +140,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
const groupTask = await member2.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
});
@@ -37,7 +37,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
});
it('returns error when task is not found', async () => {
@@ -96,7 +96,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
});
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
@@ -105,6 +105,9 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
@@ -130,20 +133,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
it('allows a user to unassign themselves', async () => {
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
// here, but then we had a request to allow to unaissgn.
xit('returns error when non leader tries to unassign their a task', async () => {
it('returns error when non leader tries to unassign a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
@@ -11,10 +10,6 @@ describe('PUT /tasks/:id', () => {
let habit;
let todo;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -44,8 +39,7 @@ describe('PUT /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
await user.post(`/tasks/${habit._id}/assign/${member2._id}`);
await user.post(`/tasks/${habit._id}/assign`, [member._id, member2._id]);
});
it('updates a group task', async () => {
@@ -56,28 +50,6 @@ describe('PUT /tasks/:id', () => {
expect(savedHabit.notes).to.eql('some new notes');
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the habit
habit = await member.put(`/tasks/${habit._id}`, {
text: 'new text!',
requiresApproval: true,
});
const memberTasks = await member2.get('/tasks/user');
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
// score up to trigger approval
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
});
it('member updates a group task value - not allowed', async () => {
// change the todo
await expect(member.put(`/tasks/${habit._id}`, {
@@ -120,7 +92,7 @@ describe('PUT /tasks/:id', () => {
],
});
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
await user.post(`/tasks/${habit._id}/assign`, [member._id]);
// change the checklist text
habit = await user.put(`/tasks/${habit._id}`, {
@@ -137,63 +109,4 @@ describe('PUT /tasks/:id', () => {
expect(habit.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
it('updates the linked tasks for all assigned users', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
expect(member2SyncedTask.text).to.eql('some new text');
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});
@@ -145,19 +145,16 @@ describe('POST /user/class/cast/:spellId', () => {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
const memberTasks = await groupLeader.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
@@ -279,7 +276,10 @@ describe('POST /user/class/cast/:spellId', () => {
type: 'todo',
});
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post('/user/class/cast/brightness');
await user.sync();
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.sync();
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await user.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
@@ -35,7 +35,7 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
message: 'Tag list must be an array.',
});
});
@@ -39,25 +39,38 @@ describe('POST /user/buy-quest/:key', () => {
}));
});
it('returns an error if quest prerequisites are not met', async () => {
const key = 'dilatoryDistress2';
it('returns an error if not all quest prerequisites are met', async () => {
const prerequisites = ['dilatoryDistress1', 'dilatoryDistress2'];
const key = 'dilatoryDistress3';
const achievementName1 = `achievements.quests.${prerequisites[0]}`;
await user.update({
[achievementName1]: true,
'stats.gp': 9999,
});
await expect(user.post(`/user/buy-quest/${key}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustComplete', { quest: 'dilatoryDistress1' }),
message: t('mustComplete', { quest: prerequisites[1] }),
});
});
it('allows purchase of a quest if prerequisites are met', async () => {
const prerequisite = 'dilatoryDistress1';
const key = 'dilatoryDistress2';
const prerequisites = ['dilatoryDistress1', 'dilatoryDistress2'];
const key = 'dilatoryDistress3';
const item = content.quests[key];
const achievementName = `achievements.quests.${prerequisite}`;
const achievementName1 = `achievements.quests.${prerequisites[0]}`;
const achievementName2 = `achievements.quests.${prerequisites[1]}`;
await user.update({ [achievementName]: true, 'stats.gp': 9999 });
await user.update({
[achievementName1]: true,
[achievementName2]: true,
'stats.gp': 9999,
});
const res = await user.post(`/user/buy-quest/${key}`);
await user.sync();
@@ -130,19 +130,16 @@ describe('POST /user/class/cast/:spellId', () => {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
const memberTasks = await groupLeader.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
@@ -247,7 +244,10 @@ describe('POST /user/class/cast/:spellId', () => {
type: 'todo',
});
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post('/user/class/cast/brightness');
await user.sync();
+4 -1
View File
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.sync();
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await user.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
+1 -1
View File
@@ -34,7 +34,7 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
message: 'Tag list must be an array.',
});
});
+24 -2
View File
@@ -33,7 +33,7 @@ describe('shared.ops.buyQuestGems', () => {
pinnedGearUtils.removeItemByPath.restore();
});
context('successful purchase', () => {
context('single purchase', () => {
const userGemAmount = 10;
before(() => {
@@ -44,7 +44,7 @@ describe('shared.ops.buyQuestGems', () => {
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
});
it('purchases quests', async () => {
it('successfully purchases quest', async () => {
const key = 'gryphon';
await buyQuest(user, { params: { key } });
@@ -58,6 +58,28 @@ describe('shared.ops.buyQuestGems', () => {
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('errors if the user has not completed prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('mustComplete', { quest: 'atom2' }));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;
user.achievements.quests.atom2 = 1;
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
+21 -1
View File
@@ -162,7 +162,9 @@ describe('shared.ops.buyQuest', () => {
}
});
it('does not buy a quest without completing previous quests', async () => {
it('returns error if user has not completed all prerequisite quests', async () => {
user.stats.gp = 9999;
user.achievements.quests.dilatoryDistress1 = 1;
try {
await buyQuest(user, {
params: {
@@ -175,4 +177,22 @@ describe('shared.ops.buyQuest', () => {
expect(user.items.quests).to.eql({});
}
});
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
user.stats.gp = 500;
user.achievements.quests.dilatoryDistress1 = 1;
user.achievements.quests.dilatoryDistress2 = 1;
await buyQuest(user, {
params: {
key: 'dilatoryDistress3',
},
}, analytics);
expect(user.items.quests).to.eql({
dilatoryDistress3: 1,
});
expect(user.stats.gp).to.equal(100);
expect(analytics.track).to.be.calledOnce;
});
});
-12
View File
@@ -249,18 +249,6 @@ describe('shared.ops.scoreTask', () => {
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
});
it('does not modify stats when task need approval', () => {
todo.group.approval.required = true;
options = {
user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false,
};
scoreTask(options);
expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
});
context('habits', () => {
it('up', () => {
options = {
+1
View File
@@ -11,6 +11,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc
setupNconf('./config.json.example');
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
nconf.set('NODE_ENV', 'test');
nconf.set('ACCOUNT_MIN_CHAT_AGE', '2');
nconf.set('IS_TEST', true);
// We require src/server and not src/index because
// 1. nconf is already setup
+188 -96
View File
@@ -22,9 +22,9 @@
}
},
"@amplitude/analytics-connector": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.4.3.tgz",
"integrity": "sha512-Ghu1UJn55Rn9eglF+ED7yOGXaeX3KY2qkQi9W9yqC02ItPvKfrybeVndweI1XtsiW0LvRpdA3uQEjuZEGunyLw==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.4.4.tgz",
"integrity": "sha512-6JcE1nxrprJt6pHqqDQb7FXRqJmFHG7KJPe0jNZaAvfll4mWKVqZu8W9IV3XiN1P+xgHIV1NN+i3PLOAZWEhXg==",
"requires": {
"@amplitude/ua-parser-js": "0.7.31"
}
@@ -1799,39 +1799,39 @@
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.17.12",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.17.12.tgz",
"integrity": "sha512-7wigcOs/Z4YWlK7xxjkvaIw84vGhDv/P1dFGQap0nHkc8gFKY/r+hXc8Qzf5k1gY7CvGIcHqAnOagVKJJ1wVOQ==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz",
"integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==",
"requires": {
"@babel/helper-plugin-utils": "^7.17.12",
"@babel/helper-skip-transparent-expression-wrappers": "^7.16.0",
"@babel/helper-plugin-utils": "^7.18.9",
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.9",
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.17.12",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz",
"integrity": "sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA=="
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz",
"integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w=="
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz",
"integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz",
"integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==",
"requires": {
"@babel/types": "^7.16.0"
"@babel/types": "^7.18.9"
}
},
"@babel/helper-validator-identifier": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
},
"@babel/types": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.0.tgz",
"integrity": "sha512-vhAmLPAiC8j9K2GnsnLPCIH5wCrPpYIVBCWRBFDCB7Y/BXLqi/O+1RSTTM2bsmg6U/551+FCf9PNPxjABmxHTw==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz",
"integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"@babel/helper-validator-identifier": "^7.18.6",
"to-fast-properties": "^2.0.0"
}
}
@@ -4382,6 +4382,49 @@
"postcss": "^7.0.0"
}
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@mdx-js/mdx": {
"version": "1.6.22",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
@@ -6446,6 +6489,11 @@
"@types/react": "*"
}
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -7148,20 +7196,14 @@
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"terser-webpack-plugin": {
@@ -9405,6 +9447,11 @@
"@types/react": "*"
}
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -9947,20 +9994,14 @@
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"terser-webpack-plugin": {
@@ -12131,11 +12172,11 @@
}
},
"@vue/cli-plugin-eslint": {
"version": "4.5.17",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.17.tgz",
"integrity": "sha512-bVNDP+SuWcuJrBMc+JLaKvlxx25XKIlZBa+zzFnxhHZlwPZ7CeBD3e2wnsygJyPoKgDZcZwDgmEz1BZzMEjsNw==",
"version": "4.5.19",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.19.tgz",
"integrity": "sha512-53sa4Pu9j5KajesFlj494CcO8vVo3e3nnZ1CCKjGGnrF90id1rUeepcFfz5XjwfEtbJZp2x/NoX/EZE6zCzSFQ==",
"requires": {
"@vue/cli-shared-utils": "^4.5.17",
"@vue/cli-shared-utils": "^4.5.19",
"eslint-loader": "^2.2.1",
"globby": "^9.2.0",
"inquirer": "^7.1.0",
@@ -12144,9 +12185,9 @@
},
"dependencies": {
"@vue/cli-shared-utils": {
"version": "4.5.17",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.17.tgz",
"integrity": "sha512-VoFNdxvTW4vZu3ne+j1Mf7mU99J2SAoRVn9XPrsouTUUJablglM8DASk7Ixhsh6ymyL/W9EADQFR6Pgj8Ujjuw==",
"version": "4.5.19",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.19.tgz",
"integrity": "sha512-JYpdsrC/d9elerKxbEUtmSSU6QRM60rirVubOewECHkBHj+tLNznWq/EhCjswywtePyLaMUK25eTqnTSZlEE+g==",
"requires": {
"@achrinza/node-ipc": "9.2.2",
"@hapi/joi": "^15.0.1",
@@ -13086,11 +13127,11 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM="
},
"amplitude-js": {
"version": "8.18.4",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.18.4.tgz",
"integrity": "sha512-Nk4ymaw9iGf1Be/fGuuH7H/QnUJceD2RYGdode8ZAApw6jHlm9QZCoYoVRrNPdgfb3yJz3P84EPh/4xM+/98/w==",
"version": "8.18.5",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.18.5.tgz",
"integrity": "sha512-s43q4qKd7kvhYESQhYvyKDKUM1PpyAyoOFFlyMuFfQHRxyeDmZRhcfzrKnOhbrLhFxSWtPc0VEeh9tajJRNe5Q==",
"requires": {
"@amplitude/analytics-connector": "1.4.3",
"@amplitude/analytics-connector": "1.4.4",
"@amplitude/ua-parser-js": "0.7.31",
"@amplitude/utils": "^1.0.5",
"@babel/runtime": "^7.3.4",
@@ -13697,9 +13738,12 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
},
"async-each": {
"version": "1.0.3",
@@ -15836,9 +15880,9 @@
}
},
"core-js": {
"version": "3.22.8",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz",
"integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA=="
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
},
"core-js-compat": {
"version": "3.11.0",
@@ -16290,7 +16334,7 @@
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
},
"debug": {
"version": "4.1.1",
@@ -16768,9 +16812,9 @@
}
},
"dompurify": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.10.tgz",
"integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g=="
},
"domutils": {
"version": "1.7.0",
@@ -20975,7 +21019,7 @@
"find-cache-dir": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz",
"integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
"integrity": "sha512-Z9XSBoNE7xQiV6MSgPuCfyMokH2K7JdpRkOYE1+mu3d4BFJtx3GW+f6Bo4q8IX6rlf5MYbLBKW0pjl2cWdkm2A==",
"requires": {
"commondir": "^1.0.1",
"mkdirp": "^0.5.1",
@@ -20985,7 +21029,7 @@
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
"integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==",
"requires": {
"path-exists": "^2.0.0",
"pinkie-promise": "^2.0.0"
@@ -20994,7 +21038,7 @@
"path-exists": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
"integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==",
"requires": {
"pinkie-promise": "^2.0.0"
}
@@ -21002,7 +21046,7 @@
"pkg-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz",
"integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
"integrity": "sha512-c6pv3OE78mcZ92ckebVDqg0aWSoKhOTbwCV6qbCWMk546mAL9pZln0+QsN/yQ7fkucd4+yJPLrCBXNt8Ruk+Eg==",
"requires": {
"find-up": "^1.0.0"
}
@@ -21926,9 +21970,9 @@
}
},
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"move-concurrently": {
"version": "1.0.1",
@@ -22035,6 +22079,11 @@
"color-convert": "^2.0.1"
}
},
"async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -23119,14 +23168,6 @@
"mkdirp": "^0.5.5"
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"requires": {
"lodash": "^4.17.14"
}
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
@@ -26883,7 +26924,7 @@
"strip-indent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
"integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g="
"integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA=="
},
"strip-json-comments": {
"version": "2.0.1",
@@ -27297,9 +27338,9 @@
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="
},
"terser": {
"version": "4.6.7",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz",
"integrity": "sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
@@ -28265,9 +28306,60 @@
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk="
},
"vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
"version": "2.7.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.8.tgz",
"integrity": "sha512-ncwlZx5qOcn754bCu5/tS/IWPhXHopfit79cx+uIlLMyt3vCMGcXai5yCG5y+I6cDmEj4ukRYyZail9FTQh7lQ==",
"requires": {
"@vue/compiler-sfc": "2.7.8",
"csstype": "^3.1.0"
},
"dependencies": {
"@babel/parser": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz",
"integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg=="
},
"@vue/compiler-sfc": {
"version": "2.7.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.8.tgz",
"integrity": "sha512-2DK4YWKfgLnW9VDR9gnju1gcYRk3flKj8UNsms7fsRmFcg35slVTZEkqwBtX+wJBXaamFfn6NxSsZh3h12Ix/Q==",
"requires": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
}
},
"csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
},
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
}
}
},
"vue-cli-plugin-storybook": {
"version": "2.1.0",
@@ -28584,12 +28676,12 @@
}
},
"vue-template-compiler": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz",
"integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==",
"version": "2.7.8",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.8.tgz",
"integrity": "sha512-eQqdcUpJKJpBRPDdxCNsqUoT0edNvdt1jFjtVnVS/LPPmr0BU2jWzXlrf6BVMeODtdLewB3j8j3WjNiB+V+giw==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.1.0"
"he": "^1.2.0"
}
},
"vue-template-es2015-compiler": {
@@ -29477,7 +29569,7 @@
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==",
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
@@ -29487,7 +29579,7 @@
"execa": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz",
"integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=",
"integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==",
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
@@ -29501,7 +29593,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
"integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ=="
},
"lru-cache": {
"version": "4.1.5",
@@ -29515,12 +29607,12 @@
"normalize-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz",
"integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k="
"integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA=="
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
}
}
},
+8 -8
View File
@@ -20,20 +20,20 @@
"@storybook/addons": "6.5.9",
"@storybook/vue": "6.3.13",
"@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^4.5.17",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-router": "^4.5.15",
"@vue/cli-plugin-unit-mocha": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.18.4",
"amplitude-js": "^8.18.5",
"axios": "^0.25.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.22.0",
"chai": "^4.3.6",
"core-js": "^3.22.8",
"dompurify": "^2.3.8",
"core-js": "^3.24.1",
"dompurify": "^2.3.10",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
@@ -44,7 +44,7 @@
"intro.js": "^5.1.0",
"jquery": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"moment": "^2.29.4",
"nconf": "^0.12.0",
"sass": "^1.34.0",
"sass-loader": "^8.0.2",
@@ -55,16 +55,16 @@
"svgo-loader": "^2.2.1",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vue": "^2.6.14",
"vue": "^2.7.8",
"vue-cli-plugin-storybook": "2.1.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.5.4",
"vue-template-compiler": "^2.6.14",
"vue-template-compiler": "^2.7.8",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^4.46.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.17.12"
"@babel/plugin-proposal-optional-chaining": "^7.18.9"
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === 'production') {
stdio: 'inherit',
});
execSync('npm run storybook:build', {
/* execSync('npm run storybook:build', {
stdio: 'inherit',
});
}); */
}
+9 -1
View File
@@ -414,7 +414,15 @@ export default {
this.$store.state.isUserLoaded = true;
Analytics.setUser();
Analytics.updateUser();
return axios.get('/api/v4/i18n/browser-script', { language: this.user.preferences.language });
return axios.get('/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
});
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
@@ -463,6 +463,11 @@
width: 48px;
height: 52px;
}
.achievement-woodlandWizard2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-woodlandWizard2x.png');
width: 60px;
height: 64px;
}
.achievement-zodiac2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-zodiac2x.png');
width: 60px;
@@ -625,6 +630,11 @@
width: 141px;
height: 147px;
}
.background_bioluminescent_waves {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_bioluminescent_waves.png');
width: 141px;
height: 147px;
}
.background_birch_forest {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_birch_forest.png');
width: 141px;
@@ -685,6 +695,11 @@
width: 141px;
height: 147px;
}
.background_by_a_campfire {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_by_a_campfire.png');
width: 141px;
height: 147px;
}
.background_camping_out {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_camping_out.png');
width: 141px;
@@ -1349,6 +1364,11 @@
width: 141px;
height: 147px;
}
.background_messy_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_messy_room.png');
width: 141px;
height: 147px;
}
.background_meteor_shower {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_meteor_shower.png');
width: 141px;
@@ -1504,6 +1524,11 @@
width: 141px;
height: 147px;
}
.background_rainbow_eucalyptus {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_eucalyptus.png');
width: 141px;
height: 147px;
}
.background_rainbow_meadow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_meadow.png');
width: 141px;
@@ -1884,11 +1909,21 @@
width: 141px;
height: 147px;
}
.background_underwater_cave {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_underwater_cave.png');
width: 141px;
height: 147px;
}
.background_underwater_ruins {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_underwater_ruins.png');
width: 141px;
height: 147px;
}
.background_underwater_statues {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_underwater_statues.png');
width: 141px;
height: 147px;
}
.background_underwater_vents {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_underwater_vents.png');
width: 141px;
@@ -2151,6 +2186,11 @@
width: 68px;
height: 68px;
}
.icon_background_bioluminescent_waves {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_bioluminescent_waves.png');
width: 68px;
height: 68px;
}
.icon_background_birch_forest {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_birch_forest.png');
width: 68px;
@@ -2211,6 +2251,11 @@
width: 68px;
height: 68px;
}
.icon_background_by_a_campfire {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_by_a_campfire.png');
width: 68px;
height: 68px;
}
.icon_background_camping_out {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_camping_out.png');
width: 68px;
@@ -2880,6 +2925,11 @@
width: 68px;
height: 68px;
}
.icon_background_messy_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_messy_room.png');
width: 68px;
height: 68px;
}
.icon_background_meteor_shower {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_meteor_shower.png');
width: 68px;
@@ -3035,6 +3085,11 @@
width: 68px;
height: 68px;
}
.icon_background_rainbow_eucalyptus {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_eucalyptus.png');
width: 68px;
height: 68px;
}
.icon_background_rainbow_meadow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_meadow.png');
width: 68px;
@@ -3420,11 +3475,21 @@
width: 68px;
height: 68px;
}
.icon_background_underwater_cave {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_underwater_cave.png');
width: 68px;
height: 68px;
}
.icon_background_underwater_ruins {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_underwater_ruins.png');
width: 68px;
height: 68px;
}
.icon_background_underwater_statues {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_underwater_statues.png');
width: 68px;
height: 68px;
}
.icon_background_underwater_vents {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_underwater_vents.png');
width: 68px;
@@ -18145,6 +18210,11 @@
width: 90px;
height: 90px;
}
.broad_armor_armoire_fancyPirateSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_fancyPirateSuit.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_farrierOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_farrierOutfit.png');
width: 90px;
@@ -18590,6 +18660,11 @@
width: 90px;
height: 90px;
}
.head_armoire_fancyPirateHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_fancyPirateHat.png');
width: 114px;
height: 90px;
}
.head_armoire_fiddlersCap {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_fiddlersCap.png');
width: 114px;
@@ -18930,6 +19005,11 @@
width: 90px;
height: 90px;
}
.shield_armoire_dustpan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dustpan.png');
width: 114px;
height: 90px;
}
.shield_armoire_fancyBlownGlassVase {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_fancyBlownGlassVase.png');
width: 114px;
@@ -19175,6 +19255,11 @@
width: 114px;
height: 87px;
}
.shield_armoire_treasureMap {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_treasureMap.png');
width: 114px;
height: 90px;
}
.shield_armoire_trustyUmbrella {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_trustyUmbrella.png');
width: 114px;
@@ -19325,6 +19410,11 @@
width: 68px;
height: 68px;
}
.shop_armor_armoire_fancyPirateSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_fancyPirateSuit.png');
width: 68px;
height: 68px;
}
.shop_armor_armoire_farrierOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_farrierOutfit.png');
width: 68px;
@@ -19785,6 +19875,11 @@
width: 68px;
height: 68px;
}
.shop_head_armoire_fancyPirateHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_fancyPirateHat.png');
width: 68px;
height: 68px;
}
.shop_head_armoire_fiddlersCap {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_fiddlersCap.png');
width: 68px;
@@ -20125,6 +20220,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_dustpan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_dustpan.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_fancyBlownGlassVase {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_fancyBlownGlassVase.png');
width: 68px;
@@ -20370,6 +20470,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_treasureMap {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_treasureMap.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_trustyUmbrella {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_trustyUmbrella.png');
width: 68px;
@@ -20505,6 +20610,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_featherDuster {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_featherDuster.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_festivalFirecracker {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_festivalFirecracker.png');
width: 68px;
@@ -20745,6 +20855,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_pushBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_pushBroom.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_rancherLasso {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_rancherLasso.png');
width: 68px;
@@ -20960,6 +21075,11 @@
width: 90px;
height: 90px;
}
.slim_armor_armoire_fancyPirateSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_fancyPirateSuit.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_farrierOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_farrierOutfit.png');
width: 90px;
@@ -21365,6 +21485,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_featherDuster {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_featherDuster.png');
width: 114px;
height: 90px;
}
.weapon_armoire_festivalFirecracker {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_festivalFirecracker.png');
width: 90px;
@@ -21605,6 +21730,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_pushBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
width: 114px;
height: 90px;
}
.weapon_armoire_rancherLasso {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_rancherLasso.png');
width: 90px;
@@ -26860,6 +26990,56 @@
width: 117px;
height: 120px;
}
.eyewear_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202208.png');
width: 114px;
height: 90px;
}
.head_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202208.png');
width: 114px;
height: 90px;
}
.shop_eyewear_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_mystery_202208.png');
width: 68px;
height: 68px;
}
.shop_head_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202208.png');
width: 68px;
height: 68px;
}
.shop_set_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202208.png');
width: 68px;
height: 68px;
}
.shield_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202209.png');
width: 114px;
height: 90px;
}
.shop_set_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202209.png');
width: 68px;
height: 68px;
}
.shop_shield_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_mystery_202209.png');
width: 68px;
height: 68px;
}
.shop_weapon_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_mystery_202209.png');
width: 68px;
height: 68px;
}
.weapon_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202209.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_301404 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
width: 90px;
@@ -36538,6 +36718,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Rainbow.png');
width: 105px;
@@ -36953,6 +37138,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Rainbow.png');
width: 105px;
@@ -37468,6 +37658,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Rainbow.png');
width: 105px;
@@ -37883,6 +38078,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Rainbow.png');
width: 105px;
@@ -38148,6 +38348,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Rainbow.png');
width: 105px;
@@ -38803,6 +39008,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Rainbow.png');
width: 105px;
@@ -39288,6 +39498,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Rainbow.png');
width: 105px;
@@ -40508,6 +40723,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Rainbow.png');
width: 105px;
@@ -41083,6 +41303,11 @@
width: 135px;
height: 135px;
}
.Mount_Body_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Porcelain.png');
width: 135px;
height: 135px;
}
.Mount_Body_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Rainbow.png');
width: 135px;
@@ -41603,6 +41828,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Rainbow.png');
width: 105px;
@@ -42018,6 +42248,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Rainbow.png');
width: 105px;
@@ -42533,6 +42768,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Rainbow.png');
width: 105px;
@@ -42948,6 +43188,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Rainbow.png');
width: 105px;
@@ -43213,6 +43458,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Rainbow.png');
width: 105px;
@@ -43868,6 +44118,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Rainbow.png');
width: 105px;
@@ -44353,6 +44608,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Rainbow.png');
width: 105px;
@@ -45573,6 +45833,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Rainbow.png');
width: 105px;
@@ -46148,6 +46413,11 @@
width: 135px;
height: 135px;
}
.Mount_Head_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Porcelain.png');
width: 135px;
height: 135px;
}
.Mount_Head_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Rainbow.png');
width: 135px;
@@ -46673,6 +46943,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Rainbow.png');
width: 81px;
@@ -47088,6 +47363,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Rainbow.png');
width: 81px;
@@ -47603,6 +47883,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Rainbow.png');
width: 81px;
@@ -48018,6 +48303,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Rainbow.png');
width: 81px;
@@ -48283,6 +48573,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Rainbow.png');
width: 81px;
@@ -48943,6 +49238,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Rainbow.png');
width: 81px;
@@ -49428,6 +49728,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Rainbow.png');
width: 81px;
@@ -50648,6 +50953,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Rainbow.png');
width: 81px;
@@ -51223,6 +51533,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Rainbow.png');
width: 81px;
@@ -51753,6 +52068,11 @@
width: 81px;
height: 99px;
}
.Pet-BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Rainbow.png');
width: 81px;
@@ -52183,6 +52503,11 @@
width: 81px;
height: 99px;
}
.Pet-Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Rainbow.png');
width: 81px;
@@ -52718,6 +53043,11 @@
width: 81px;
height: 99px;
}
.Pet-Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Rainbow.png');
width: 81px;
@@ -53148,6 +53478,11 @@
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Rainbow.png');
width: 81px;
@@ -53428,6 +53763,11 @@
width: 81px;
height: 99px;
}
.Pet-Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Rainbow.png');
width: 81px;
@@ -54103,6 +54443,11 @@
width: 81px;
height: 99px;
}
.Pet-LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Rainbow.png');
width: 81px;
@@ -54603,6 +54948,11 @@
width: 81px;
height: 99px;
}
.Pet-PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Rainbow.png');
width: 81px;
@@ -55848,6 +56198,11 @@
width: 81px;
height: 99px;
}
.Pet-TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Rainbow.png');
width: 81px;
@@ -56443,6 +56798,11 @@
width: 81px;
height: 99px;
}
.Pet-Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Rainbow.png');
width: 81px;
@@ -56773,6 +57133,11 @@
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Porcelain.png');
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Purple {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Purple.png');
width: 68px;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

+53 -62
View File
@@ -1,9 +1,56 @@
.create-task-area {
position: absolute;
right: 24px;
right: 12px;
top: -24px;
z-index: 998;
align-items: center;
.dropdown {
width: 140px;
border-radius: 2px;
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
background-color: $white;
margin-top: 2px;
position: absolute;
right: 0px;
.task-icon {
width: 30px;
}
.task-label {
margin-top: 1px;
}
.dropdown-item:hover {
.svg-icon {
color: $purple-300;
}
}
.svg-icon {
color: $gray-200;
&.icon-habit {
width: 30px;
height: 20px;
}
&.icon-daily {
width: 24px;
height: 20px;
}
&.icon-todo {
width: 20px;
height: 20px;
}
&.icon-reward {
width: 26px;
height: 20px;
}
}
}
}
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
@@ -17,70 +64,14 @@
overflow-x: hidden;
}
.diamond-btn {
margin-left: 24px;
background: $white;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
cursor: pointer;
color: $gray-200;
transform: rotate(45deg);
&:hover:not(.create-btn) {
color: $purple-400;
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
}
.svg-icon {
width: 20px;
height: 20px;
transform: rotate(-45deg);
&.icon-habit {
width: 24px;
height: 16px;
}
&.icon-daily {
width: 21.6px;
height: 18px;
}
&.icon-todo {
width: 18px;
height: 18px;
}
&.icon-reward {
width: 23.4px;
height: 18px;
}
}
}
.create-btn {
color: $white;
background-color: $green-100;
height: 48px;
width: 48px;
background-color: $purple-200;
height: 32px;
.svg-icon {
width: 16px;
height: 16px;
transform: rotate(-45deg);
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
}
&.open {
background: $gray-200 !important;
.svg-icon {
transform: rotate(-90deg);
}
color: $purple-500;
width: 10px;
height: 10px;
}
}
+76 -26
View File
@@ -82,7 +82,7 @@ input, textarea, input.form-control, textarea.form-control {
}
}
/** Colored Input-Groups, ignoring checklist */
// Colored Input-Groups, ignoring checklist
.input-group:not(.checklist-group) {
border-radius: 2px;
border: solid 1px $gray-400;
@@ -100,7 +100,7 @@ input, textarea, input.form-control, textarea.form-control {
}
}
/** Generic Input Group Styles */
// Generic Input Group Styles
.input-group {
height: 2rem;
@@ -179,10 +179,11 @@ input, textarea, input.form-control, textarea.form-control {
padding-left: 0px;
}
// Checkboxes and radios
// used in checkboxes and radios
$bg-focused-active-control: #4f2993;
$bg-disabled-control: #34303a;
// custom control
.custom-control {
margin-bottom: .5rem;
@@ -205,6 +206,7 @@ $bg-disabled-control: #34303a;
}
}
// checkboxes
.custom-checkbox {
.custom-control-label::before {
border-radius: 2px;
@@ -280,11 +282,26 @@ $bg-disabled-control: #34303a;
padding-left: 36px;
}
// radio buttons
$bg-color: $purple-400;
// svg for the purple dot
@mixin custom-radio-checked-icon ($bg-color) {
background-image: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$bg-color}'/%3E%3C/svg%3E"), "#", "%23");
}
.custom-radio .custom-control-input {
opacity: 0;
margin: 15px 25px 34px 25px;
// outside circle
&:checked~.custom-control-label::before {
background-color: $gray-700;
background-size: 12px 12px;
border-color: $purple-400;
}
// checked indicator
&:checked~.custom-control-label::after {
@include custom-radio-checked-icon($purple-400);
width: 18px;
@@ -292,51 +309,84 @@ $bg-disabled-control: #34303a;
background-size: 12px 12px;
}
&:checked~.custom-control-label::before {
background-color: $gray-700;
background-size: 12px 12px;
border-color: $purple-400;
}
&:active~.custom-control-label::before {
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-label::before, &:active:not(:checked):not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
// focus / not checked / not disabled
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:focus:checked:not(:disabled)~.custom-control-label::before, &:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
border-color: $purple-400;
background-color: rgba($bg-focused-active-control, 0.1);
// focus / checked / not disabled
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:active:checked:not(:disabled)~.custom-control-label::before {
border: 2px solid $purple-400;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:disabled:checked~.custom-control-label::before {
border-color: $gray-400;
background-color: transparent;
// hover / not checked / not disabled
&:hover:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
width: 18px;
height: 18px;
background: 50%/50% 50% no-repeat;
@include custom-radio-checked-icon($purple-400);
background-size: 12px 12px;
border: solid 2px $purple-400;
}
&:disabled:checked~.custom-control-label::after {
// hover / checked / not disabled
&:hover:checked:not(:disabled)~.custom-control-label::before,
&:active::checked:not(:disabled)~.custom-control-label::before {
width: 18px;
height: 18px;
background: 50%/50% 50% no-repeat;
@include custom-radio-checked-icon($gray-400);
background-size: 12px 12px;
border: solid 2px $purple-300;
}
// disabled / checked / before
&:disabled:checked~.custom-control-label::before {
background: 50%/50% 50% no-repeat;
@include custom-radio-checked-icon($gray-300);
border: 2px solid $gray-200;
background-color: transparent;
opacity: 0.75;
}
// disabled / checked / after
&:disabled:checked~.custom-control-label::after {
background: 50%/50% 50% no-repeat;
@include custom-radio-checked-icon($gray-300);
width: 18px;
height: 18px;
background-size: 12px 12px;
}
// disabled / not checked / before
&:disabled:not(:checked)~.custom-control-label::before {
border-color: $gray-300;
background-color: transparent;
background-color: $gray-600;
border: 2px solid $gray-200;
}
&:focus:disabled~.custom-control-label::before, &:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
border-color: $gray-300;
// focus and disabled / not checked / before
&:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before {
background-color: rgba($bg-disabled-control, 0.1);
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
border: 2px solid $gray-200;
}
&:focus:disabled:checked~.custom-control-label::before, &:active:disabled:checked~.custom-control-label::before {
border-color: $gray-400;
// focus and disabled / checked / before
&:focus:disabled:checked~.custom-control-label::before,
&:active:disabled:checked~.custom-control-label::before {
background: 50%/50% 50% no-repeat;
@include custom-radio-checked-icon($gray-300);
border: 2px solid $gray-200;
}
}
+15 -15
View File
@@ -30,7 +30,7 @@
&-bg {
background: $maroon-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $maroon-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -41,7 +41,7 @@
&-modal {
&-bg { background: $maroon-100 !important; }
&-headings { color: $white; }
&-headings { color: $white !important; }
&-icon { color: $maroon-100 !important; }
&-text { color: $red-1 !important; }
&-content {
@@ -61,7 +61,7 @@
&-bg {
background: $red-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $red-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -92,7 +92,7 @@
&-bg {
background: $orange-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $orange-100 !important; }
&-inner-habit { background: rgba($orange-1, 0.25) !important; }
@@ -123,7 +123,7 @@
&-bg {
background: $yellow-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $yellow-100 !important; }
&-inner-habit { background: rgba($yellow-1, 0.25) !important; }
@@ -154,7 +154,7 @@
&-bg {
background: $green-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $green-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -186,7 +186,7 @@
&-bg {
background: $teal-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $teal-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -217,7 +217,7 @@
&-bg {
background: $blue-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $blue-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -249,7 +249,7 @@
&-bg {
background: $purple-task !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-inner-habit { background: rgba($black, 0.25) !important; }
&-inner-daily-todo { background: rgba($white, 0.5) !important; }
@@ -258,7 +258,7 @@
&-modal {
&-bg { background: $purple-300 !important; }
&-headings { color: $white; }
&-headings { color: $white !important; }
&-icon { color: $purple-300 !important; }
&-text { color: $black !important; }
&-content {
@@ -281,11 +281,11 @@
background: rgba($yellow-100, 0.15) !important;
.small-text { color: $yellow-1 !important; }
&:hover { background: rgba($yellow-100, 0.25) !important; }
&:hover:not(.task-not-scoreable) { background: rgba($yellow-100, 0.25) !important; }
}
&-bg-noninteractive {
background: rgba($yellow-100, 0.15) !important;
.small-text { color: $yellow-1 !important; }
background-color: $gray-500 !important;
.small-text { color: $gray-100 !important; }
}
}
}
@@ -312,7 +312,7 @@
&-control {
&-bg {
background: $gray-200 !important;
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive {
background: $gray-200 !important;
@@ -323,7 +323,7 @@
background: $gray-600;
.task-title, .task-notes {
color: $gray-300 !important;
opacity: 0.75;
}
}
}
@@ -0,0 +1,27 @@
<svg width="176" height="67" viewBox="0 0 176 67" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path fill="#77F4C7" d="M35.667 11.667 40 9.5l-4.333-2.167L33.5 3l-2.167 4.333L27 9.5l4.333 2.167L33.5 16z"/>
<path fill="#BDA8FF" d="M24.667 38.667 30 36l-5.333-2.667L22 28l-2.667 5.333L14 36l5.333 2.667L22 44z"/>
<path fill="#8EEDF6" d="M35.667 63.667 39 62l-3.333-1.667L34 57l-1.667 3.333L29 62l3.333 1.667L34 67z"/>
<path fill="#FFBE5D" d="M6.667 49.667 10 48l-3.333-1.667L5 43l-1.667 3.333L0 48l3.333 1.667L5 53z"/>
<path fill="#FFB6B8" d="M5.667 20.667 8 19.5l-2.333-1.167L4.5 16l-1.167 2.333L1 19.5l2.333 1.167L4.5 23z"/>
<g>
<path fill="#77F4C7" d="M140.333 11.667 136 9.5l4.333-2.167L142.5 3l2.167 4.333L149 9.5l-4.333 2.167L142.5 16z"/>
<path fill="#BDA8FF" d="M151.333 38.667 146 36l5.333-2.667L154 28l2.667 5.333L162 36l-5.333 2.667L154 44z"/>
<path fill="#8EEDF6" d="M140.333 63.667 137 62l3.333-1.667L142 57l1.667 3.333L147 62l-3.333 1.667L142 67z"/>
<path fill="#FFBE5D" d="M169.333 49.667 166 48l3.333-1.667L171 43l1.667 3.333L176 48l-3.333 1.667L171 53z"/>
<path fill="#FFB6B8" d="M170.333 20.667 168 19.5l2.333-1.167L171.5 16l1.167 2.333L175 19.5l-2.333 1.167L171.5 23z"/>
</g>
<g>
<path d="M81.117 13.904c-2.139-4.838-6.274-9.113-11.25-9.324-4.976-.211-7.828 3.779-6.367 7.309 1.461 3.53 4.94 4.177 16.227 7.202 3.204.858 3.528-.35 1.39-5.187z" stroke="#22AEB7" stroke-width="4"/>
<path d="M93.833 13.904c2.138-4.838 6.273-9.113 11.25-9.324 4.975-.211 7.828 3.779 6.367 7.309-1.462 3.53-4.94 4.177-16.227 7.202-3.205.858-3.528-.35-1.39-5.187z" stroke="#38C9C6" stroke-width="4"/>
<path d="M87.128 11c-9.738 0-3.907 11.145 0 11.145 3.908 0 9.74-11.145 0-11.145z" fill="#46DDDA"/>
<path fill="#6133B4" d="M62 33h52v34H62zM56 21h64v12H56z"/>
<path fill-opacity=".5" fill="#FFF" style="mix-blend-mode:soft-light" d="M32 30h26v34H32z" transform="translate(56 3)"/>
<path fill="#8EEDF6" d="M88 33h6v34h-6z"/>
<path fill="#3BCAD7" d="M82 33h6v34h-6zM76 21h12v12H76z"/>
<path fill="#8EEDF6" d="M88 21h12v12H88z"/>
<path fill-opacity=".2" fill="#000" style="mix-blend-mode:multiply" d="M6 30h26v6H6zM20 18h12v6H20zM0 24h20v6H0zM44 24h20v6H44zM32 18h12v6H32zM6 58h26v6H6zM32 30h26v6H32zM32 58h26v6H32z" transform="translate(56 3)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+10 -4
View File
@@ -1,5 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="2">
<path d="M1 11L11 1M11 11L1 1"/>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon/Close</title>
<g id="Modals" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Shop-Modals" transform="translate(-183.000000, -655.000000)" fill="#878190" fill-rule="nonzero">
<g id="Icon/Close" transform="translate(183.000000, 655.000000)">
<polygon id="Mask" points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 747 B

@@ -0,0 +1,3 @@
<svg width="16" height="14" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
<path d="m12.707 5.707-1.414-1.414L8 7.586 6.707 6.293 5.293 7.707 8 10.414l4.707-4.707zM16 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2h2v2h10V2H4v2.5h2l-3 3-3-3h2V2a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" fill="#878190" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

+1 -20
View File
@@ -1,20 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="rt3mruthma" d="M11.2 13.2c0-1.231-.467-2.35-1.23-3.2h1.23c1.765 0 3.2 1.435 3.2 3.2h-3.2zm-9.6 0c0-1.765 1.435-3.2 3.2-3.2h1.6c1.765 0 3.2 1.435 3.2 3.2h-8zm4-9.6C6.926 3.6 8 4.674 8 6c0 1.326-1.074 2.4-2.4 2.4-1.326 0-2.4-1.074-2.4-2.4 0-1.326 1.074-2.4 2.4-2.4zm3.452.415C9.436 3.754 9.9 3.6 10.4 3.6c1.326 0 2.4 1.074 2.4 2.4 0 1.326-1.074 2.4-2.4 2.4-.5 0-.964-.154-1.348-.415.34-.587.548-1.26.548-1.985 0-.726-.209-1.398-.548-1.985zm4.154 4.829C13.942 8.118 14.4 7.112 14.4 6c0-2.206-1.794-4-4-4-.904 0-1.73.313-2.4.82C7.33 2.314 6.504 2 5.6 2c-2.206 0-4 1.794-4 4 0 1.112.458 2.118 1.194 2.844C1.146 9.604 0 11.266 0 13.2c0 .884.716 1.6 1.6 1.6h12.8c.884 0 1.6-.716 1.6-1.6 0-1.934-1.146-3.596-2.794-4.356z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g transform="translate(-1228 -1456) translate(1216 1416) translate(12 40)">
<mask id="6szqyv7o1b" fill="#fff">
<use xlink:href="#rt3mruthma"/>
</mask>
<use fill="#878190" xlink:href="#rt3mruthma"/>
<g fill="#878190" mask="url(#6szqyv7o1b)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12.8"><path d="M11.2,12.8A4.79,4.79,0,0,0,10,9.6H11.2a3.21,3.21,0,0,1,3.2,3.2Zm-9.6,0A3.21,3.21,0,0,1,4.8,9.6H6.4a3.21,3.21,0,0,1,3.2,3.2Zm4-9.6A2.4,2.4,0,1,1,3.2,5.6,2.39,2.39,0,0,1,5.6,3.2Zm3.45.42A2.35,2.35,0,0,1,10.4,3.2a2.4,2.4,0,1,1,0,4.8,2.35,2.35,0,0,1-1.35-.42,3.84,3.84,0,0,0,0-4Zm4.16,4.82A4,4,0,0,0,8,2.42,4,4,0,0,0,5.6,1.6,4,4,0,0,0,2.79,8.44,4.82,4.82,0,0,0,0,12.8a1.6,1.6,0,0,0,1.6,1.6H14.4A1.6,1.6,0,0,0,16,12.8a4.82,4.82,0,0,0-2.79-4.36Z" transform="translate(0 -1.6)" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 569 B

@@ -757,8 +757,7 @@ export default {
}, 500),
sanitizeRedirect (redirect) {
if (!redirect) return '/';
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
sanitizedString = `/${sanitizedString}`;
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
return sanitizedString;
},
async register () {
@@ -112,10 +112,8 @@ export default {
},
},
watch: {
'group._id': {
async groupId () {
this.loadChallenges();
},
'group._id': function groupId () {
this.loadChallenges();
},
},
mounted () {
@@ -122,6 +122,11 @@
font-weight: bold;
}
.custom-control-input {
z-index: -1;
opacity: 0;
}
.box:hover {
cursor: pointer;
opacity: 0.7;
@@ -0,0 +1,204 @@
<template>
<b-modal
id="group-plans-update"
title="New Shared Task Board"
size="lg"
hide-footer="hide-footer"
:no-close-on-backdrop="true"
:no-close-on-esc="true"
:centered="true"
>
<div
slot="modal-header"
class="w-100 d-flex pt-2 justify-content-center"
>
<h2
class="mx-auto mt-4"
v-once
>
{{ $t('newGroupsWelcome') }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
aria-hidden="true"
tabindex="0"
@click="close()"
@keypress.enter="close()"
v-html="icons.close"
></div>
<img
class="task-columns"
src="~@/assets/images/group-plans/task-columns.png"
srcset="~@/assets/images/group-plans/task-columns@2x.png 2x,
~@/assets/images/group-plans/task-columns@3x.png 3x"
>
</div>
<div
class="d-flex flex-column justify-content-center p-4"
>
<div class="d-flex align-items-center justify-content-center mb-3">
<div
class="svg-icon sparkles my-auto mr-3"
v-html="icons.sparkles"
>
</div>
<h3
class="my-auto"
v-once
>
{{ $t('newGroupsWhatsNew') }}
</h3>
<div
class="svg-icon sparkles my-auto ml-3 flip-x"
v-html="icons.sparkles"
>
</div>
</div>
<ul
class="mb-4 px-4"
>
<li>{{ $t('newGroupsBullet01') }}</li>
<li>{{ $t('newGroupsBullet02') }}</li>
<li>{{ $t('newGroupsBullet03') }}</li>
<li>{{ $t('newGroupsBullet04') }}</li>
<li>{{ $t('newGroupsBullet05') }}</li>
<li>{{ $t('newGroupsBullet06') }}</li>
<li>{{ $t('newGroupsBullet07') }}</li>
<li>{{ $t('newGroupsBullet08') }}</li>
<li>{{ $t('newGroupsBullet09') }}</li>
<li>{{ $t('newGroupsBullet10') }}
<ul class="p-0">
<li v-html="$t('newGroupsBullet10a')"></li>
<li v-html="$t('newGroupsBullet10b')"></li>
<li v-html="$t('newGroupsBullet10c')"></li>
</ul>
</li>
</ul>
<div
class="mx-auto"
v-html="$t('newGroupsVisitFAQ')"
></div>
<div
class="mx-auto"
>
{{ $t('newGroupsEnjoy') }}
</div>
<button
class="btn btn-primary mt-4 mb-1 mx-auto"
@click="close()"
@keypress.enter="close()"
>
{{ $t('getStarted') }}
</button>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#group-plans-update {
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-dialog {
max-width: 566px;
margin-top: 0px;
}
.modal-header {
background-image: linear-gradient(64deg, $purple-200, $purple-300 55%);
height: 136px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $white;
line-height: 28px;
}
li {
padding-left: 0.75rem;
}
ul {
line-height: 24px;
list-style-type: square;
li ul {
list-style-type: none;
li::before {
margin-right: 1rem;
font-size: 16px;
content: '▫';
}
}
}
.btn-primary {
width: max-content;
}
.close-x {
color: $purple-400;
height: 16px;
width: 16px;
position: absolute;
opacity: 0.75;
cursor: pointer;
right: 18px;
top: 18px;
&:hover, &:focus {
opacity: 1;
}
}
.sparkles {
width: 32px;
height: 17px;
&.flip-x {
transform: scaleX(-1);
}
}
.task-columns {
position: absolute;
top: 84px;
}
</style>
<script>
import closeIcon from '@/assets/svg/close.svg';
import sparkles from '@/assets/svg/sparkles-left.svg';
export default {
data () {
return {
icons: Object.freeze({
close: closeIcon,
sparkles,
}),
};
},
methods: {
close () {
this.$store.dispatch('user:set', { 'flags.tour.groupPlans': -2 });
this.$root.$emit('bv::hide::modal', 'group-plans-update');
},
},
};
</script>
@@ -28,7 +28,7 @@
{{ $t('groupBilling') }}
</router-link>
</secondary-menu>
<div class="col-12">
<div class="col-12 px-0">
<router-view />
</div>
</div>
@@ -1,5 +1,8 @@
<template>
<div class="standard-page">
<div
class="standard-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<group-plan-overview-modal />
<task-modal
ref="taskModal"
@@ -7,65 +10,80 @@
:purpose="taskFormPurpose"
:group-id="groupId"
@cancel="cancelTaskModal()"
@taskCreated="taskCreated"
@taskEdited="taskEdited"
@taskCreated="loadTasks"
@taskEdited="loadTasks"
@taskDestroyed="taskDestroyed"
/>
<div class="row tasks-navigation">
<div class="col-12 col-md-4">
<h1>{{ $t('groupTasksTitle') }}</h1>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="d-flex flex-wrap align-items-center mb-4">
<div class="mr-auto">
<h1>{{ group.name }}</h1>
</div>
<!-- @TODO: Abstract to component!-->
<div class="col-12 col-md-4">
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
</div>
<div
v-if="canCreateTasks"
class="create-task-area d-flex"
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
<transition name="slide-tasks-btns">
<div
class="d-flex flex-wrap align-items-center justify-content-end ml-auto"
>
<toggle-switch
id="taskMirrorToggle"
class="mr-3 mb-1 ml-auto"
:label="'Copy tasks'"
:checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1"
:hover-text="'Show assigned and open tasks on your personal task board'"
@change="changeMirrorPreference"
/>
<div
class="day-start d-flex align-items-center"
v-html="$t('dayStart', { startTime: groupStartTime } )"
>
</div>
<div class="ml-2">
<button
id="create-task-btn"
v-if="canCreateTasks"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</button>
<div
v-if="openCreateBtn"
class="d-flex"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
class="svg-icon"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('create') }}
</b-tooltip>
</div>
</div>
<div class="row">
@@ -79,6 +97,7 @@
:search-text="searchText"
:draggable-override="canCreateTasks"
@editTask="editTask"
@taskSummary="taskSummary"
@loadGroupCompletedTodos="loadGroupCompletedTodos"
@taskDestroyed="taskDestroyed"
/>
@@ -86,12 +105,58 @@
</div>
</template>
<style lang="scss">
#taskMirrorToggle {
font-weight: bold;
.svg-icon {
margin: 3px 6px 0px 4px;
}
.toggle-switch {
margin-left: 0px;
}
.toggle-switch-description {
margin-top: 3px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/create-task.scss';
.tasks-navigation {
margin-bottom: 40px;
h1 {
color: $purple-300;
margin-bottom: 0px;
}
.day-start {
height: 2rem;
padding: 0.25rem 0.75rem;
border-radius: 2px;
color: $gray-100;
background-color: $gray-600;
}
@media screen and (min-width: 1200px) {
.input-search {
margin-left: 12.5rem;
width: 25%;
}
}
@media screen and (max-width: 1200px) {
.input-search {
width: 50%;
}
}
@media screen and (max-width: 500px) {
#create-task-btn {
margin-top: 4px;
}
}
.positive {
@@ -107,11 +172,15 @@
import Vue from 'vue';
import cloneDeep from 'lodash/cloneDeep';
import findIndex from 'lodash/findIndex';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import TaskColumn from '../tasks/column';
import TaskModal from '../tasks/taskModal';
import TaskSummary from '../tasks/taskSummary';
import GroupPlanOverviewModal from './groupPlanOverviewModal';
import toggleSwitch from '@/components/ui/toggleSwitch';
import sync from '../../mixins/sync';
import positiveIcon from '@/assets/svg/positive.svg';
import filterIcon from '@/assets/svg/filter.svg';
@@ -121,14 +190,18 @@ import dailyIcon from '@/assets/svg/daily.svg';
import todoIcon from '@/assets/svg/todo.svg';
import rewardIcon from '@/assets/svg/reward.svg';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
GroupPlanOverviewModal,
toggleSwitch,
},
mixins: [sync],
props: ['groupId'],
data () {
return {
@@ -199,6 +272,16 @@ export default {
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
groupStartTime () {
if (!this.group || !this.group.cron) return null;
const { dayStart, timezoneOffset } = this.group.cron;
const timezoneDiff = this.user.preferences.timezoneOffset - timezoneOffset;
return moment()
.hour(dayStart)
.minute(0)
.subtract(timezoneDiff, 'minutes')
.format('h:mm A');
},
},
watch: {
// call again the method if the route changes (when this route is already active)
@@ -208,6 +291,10 @@ export default {
this.$set(this, 'searchId', to.params.groupId);
next();
},
async beforeRouteLeave (to, from, next) {
await this.sync();
next();
},
mounted () {
if (!this.searchId) this.searchId = this.groupId;
this.load();
@@ -218,6 +305,7 @@ export default {
this.$root.$on('habitica:team-sync', () => {
this.loadTasks();
this.loadGroupCompletedTodos();
});
},
methods: {
@@ -238,6 +326,9 @@ export default {
this.group.members = members;
this.loadTasks();
if (this.user.flags.tour.groupPlans !== -2) {
this.$root.$emit('bv::show::modal', 'group-plans-update');
}
},
async loadTasks () {
this.tasksByType = {
@@ -251,22 +342,13 @@ export default {
groupId: this.searchId,
});
const groupedApprovals = await this.loadApprovals();
tasks.forEach(task => {
if (
groupedApprovals[task._id]
&& groupedApprovals[task._id].length > 0
) task.approvals = groupedApprovals[task._id];
this.tasksByType[task.type].push(task);
});
},
async loadApprovals () {
const approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
groupId: this.searchId,
});
return groupBy(approvalRequests, 'group.taskId');
if (this.editingTask && this.editingTask.completed) {
this.loadGroupCompletedTodos();
}
},
editTask (task) {
this.taskFormPurpose = 'edit';
@@ -277,6 +359,12 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
async loadGroupCompletedTodos () {
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
groupId: this.searchId,
@@ -299,14 +387,6 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskCreated (task) {
task.group.id = this.group._id;
this.tasksByType[task.type].unshift(task);
},
taskEdited (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1, task);
},
taskDestroyed (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1);
@@ -354,6 +434,25 @@ export default {
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
} else { // we're turning copy OFF for this group
groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1);
}
this.$store.dispatch('user:set', {
'preferences.tasks.mirrorGroupTasks': groupsToMirror,
});
},
},
};
</script>
@@ -247,9 +247,6 @@
<li v-once>
{{ $t('sleepBullet3') }}
</li>
<li v-once>
{{ $t('sleepBullet4') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
@@ -342,6 +339,7 @@
<li>
<a
href
:style="glossary-link"
v-html="$t('glossary')"
></a>
</li>
@@ -536,6 +534,21 @@
margin-left: .5em;
}
// formats the report a bug link to match the others
a:not([href]) {
&:not([role=button]) {
color: #007bff;
text-decoration: none;
}
}
a:not([href]):hover {
&:not([role=button]) {
color: #0056b3;
text-decoration: underline;
}
}
.tier1-icon, .tier2-icon {
width: 11px;
}
@@ -3,7 +3,7 @@
<creator-intro />
<profileModal />
<report-flag-modal />
<send-gems-modal />
<send-gift-modal />
<select-user-modal />
<b-navbar
id="habitica-menu"
@@ -747,7 +747,7 @@ import creatorIntro from '../creatorIntro';
import notificationMenu from './notificationsDropdown';
import profileModal from '../userMenu/profileModal';
import reportFlagModal from '../chat/reportFlagModal';
import sendGemsModal from '@/components/payments/sendGemsModal';
import sendGiftModal from '@/components/payments/sendGiftModal';
import selectUserModal from '@/components/payments/selectUserModal';
import sync from '@/mixins/sync';
import userDropdown from './userDropdown';
@@ -759,7 +759,7 @@ export default {
notificationMenu,
profileModal,
reportFlagModal,
sendGemsModal,
sendGiftModal,
selectUserModal,
userDropdown,
},
@@ -23,7 +23,14 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push({ name: 'tasks' });
if (this.notification.data.groupId) {
this.$router.push({
name: 'groupPlanDetailTaskInformation',
params: { groupId: this.notification.data.groupId },
});
} else {
this.$router.push({ name: 'tasks' });
}
},
},
};
@@ -129,8 +129,6 @@ import GUILD_INVITATION from './notifications/guildInvitation';
import PARTY_INVITATION from './notifications/partyInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
@@ -155,8 +153,6 @@ export default {
PARTY_INVITATION,
CHALLENGE_INVITATION,
QUEST_INVITATION,
GROUP_TASK_APPROVAL,
GROUP_TASK_APPROVED,
GROUP_TASK_ASSIGNED,
GROUP_TASK_CLAIMED,
UNALLOCATED_STATS_POINTS,
@@ -182,7 +178,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVED',
'QUEST_INVITATION',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
@@ -196,8 +192,6 @@ export default {
'CHALLENGE_INVITATION',
'QUEST_INVITATION',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED',
'NEW_MYSTERY_ITEMS',
'CARD_RECEIVED',
@@ -4,7 +4,7 @@
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }"
@click="showMemberModal(member)"
>
<div :class="{ 'col-4': !isHeader }">
<div class="avatar-container" :class="{ 'col-4': !isHeader }">
<avatar
:member="member"
:hide-class-badge="classBadgePosition !== 'under-avatar'"
@@ -92,6 +92,10 @@
.member-details {
white-space: nowrap;
transition: all 0.15s ease-out;
.avatar-container {
margin-bottom: 20px;
}
}
.member-stats {
@@ -32,11 +32,12 @@
/>
<onboarding-complete />
<first-drops />
<group-plans-update />
</div>
</template>
<style lang='scss'>
.introjs-helperNumberLayer, .introjs-bullets {
.introjs-helperNumberLayer, .introjs-bullets, .introjs-skipbutton {
display: none;
}
@@ -62,9 +63,9 @@
.npc_justin_textbox {
position: absolute;
right: 1em;
top: -3.6em;
top: -55px;
width: 48px;
height: 52px;
height: 51px;
background-image: url('~@/assets/images/justin_textbox.png');
}
}
@@ -95,7 +96,7 @@
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12) !important;
}
.introjs-skipbutton.btn-primary, .introjs-donebutton.btn-primary {
.introjs-donebutton.btn-primary {
color: #fff;
}
</style>
@@ -142,6 +143,7 @@ import loginIncentives from './achievements/login-incentives';
import onboardingComplete from './achievements/onboardingComplete';
import verifyUsername from './settings/verifyUsername';
import firstDrops from './achievements/firstDrops';
import groupPlansUpdate from './group-plans/groupPlansUpdateModal';
const NOTIFICATIONS = {
// general notifications
@@ -268,6 +270,7 @@ export default {
genericAchievement,
onboardingComplete,
firstDrops,
groupPlansUpdate,
},
mixins: [notifications, guide],
data () {
@@ -612,7 +615,7 @@ export default {
const yesterUtcOffset = yesterDay.utcOffset();
dailys.forEach(task => {
if (task && task.group.approval && task.group.approval.requested) return;
if (task.group && task.group.id) return;
if (task.completed) return;
const due = shouldDo(yesterDay, task, { timezoneUtcOffset: yesterUtcOffset });
if (task.yesterDaily && due) this.yesterDailies.push(task);
@@ -198,7 +198,7 @@ export default {
appState.newGroup = false;
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name']);
}
} else if (paymentType.indexOf('gift-') === 0) {
} else if (paymentType && paymentType.indexOf('gift-') === 0) {
appState.gift = this.amazonPayments.gift;
appState.giftReceiver = this.amazonPayments.giftReceiver;
} else if (paymentType === 'gems') {
@@ -1,5 +1,6 @@
<template>
<div class="payments-column mx-auto mt-auto">
<h4>{{ $t('choosePaymentMethod') }}</h4>
<button
v-if="stripeAvailable"
class="btn btn-primary payment-button payment-item with-icon"
@@ -80,6 +81,13 @@
cursor: default !important;
}
}
h4 {
font-size: 0.875rem;
font-weight: bold;
text-align: center;
margin-bottom: 1rem;
}
</style>
<script>
@@ -14,15 +14,44 @@
</div>
<h2
v-else
class="ml-2"
class="d-flex flex-column mx-auto align-items-center"
>
{{ $t('sendGift') }}
{{ $t('sendAGift') }}
</h2>
<div
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-margin d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
</div>
<div
v-else
class="d-flex flex-column align-items-center">
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
</div>
<div class="d-flex flex-column align-items-center">
<div
class="modal-close"
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-modal-close"
@click="close()"
>
<div
class="g1g1-svg-icon"
v-html="icons.close"
></div>
</div>
<div
v-else
class="modal-close"
@click="close()">
<div
class="svg-icon"
v-html="icons.close"
@@ -42,6 +71,7 @@
v-model="userSearchTerm"
class="form-control"
type="text"
ref="textBox"
:placeholder="$t('usernameOrUserId')"
:class="{
'input-valid': foundUser._id,
@@ -70,15 +100,20 @@
<div
v-else
>
{{ $t('selectGift') }}
{{ $t('next') }}
</div>
</button>
<a
class="cancel-link mx-auto mt-3"
@click="close()"
<div
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
v-html="$t('cancel')"
@click="close()"
>
{{ $t('cancel') }}
</a>
{{ $t('cancel') }}
</div>
<div
v-else>
</div>
</div>
</div>
</div>
@@ -110,13 +145,16 @@
@import '~@/assets/scss/mixins.scss';
#select-user-modal {
.modal-content {
width:448px;
}
.input-group {
margin-top: 0rem;
}
.modal-dialog {
width: 29.5rem;
margin-top: 25vh;
width: 448px;
}
.modal-footer {
@@ -126,7 +164,16 @@
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
}
</style>
<style lang="scss" scoped>
@@ -146,12 +193,12 @@
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 472px 152px;
width: 470px;
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -1rem 0rem 0rem -1rem;
border-radius: 0.3rem 0.3rem 0rem 0rem;
padding: 1.5rem;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
color: $white;
h1 {
@@ -169,6 +216,16 @@
}
}
.g1g1-margin {
margin-top: 24px;
}
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
@@ -176,6 +233,29 @@
line-height: 1.33;
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-svg-icon {
width: 12px;
height: 12px;
& ::v-deep svg path {
fill: #FFFFFF;
}
}
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-error {
color: $red-50;
font-size: 90%;
@@ -192,6 +272,18 @@
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
@@ -206,14 +298,17 @@
height: 12px;
}
}
</style>
<script>
// import { nextTick } from 'vue'; // may not need this? I don't know!
import debounce from 'lodash/debounce';
import find from 'lodash/find';
import isUUID from 'validator/lib/isUUID';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
import bigGiftIcon from '@/assets/svg/big-gift.svg';
export default {
data () {
@@ -223,6 +318,7 @@ export default {
foundUser: {},
icons: Object.freeze({
close: closeIcon,
bigGift: bigGiftIcon,
}),
};
},
@@ -281,7 +377,7 @@ export default {
this.foundUser = result;
}, 500),
selectUser () {
this.$root.$emit('habitica::send-gems', this.foundUser);
this.$root.$emit('habitica::send-gift', this.foundUser);
this.close();
},
onHide () {
@@ -0,0 +1,659 @@
<template>
<b-modal
id="send-gift"
:hide-footer="true"
:hide-header="true"
size="md"
@hide="onHide()"
>
<div>
<!-- header -->
<div
class="modal-close"
@click="close()"
>
<div
class="icon-close"
v-html="icons.closeIcon"
>
</div>
</div>
<div>
<h2 class="d-flex flex-column mx-auto align-items-center">
{{ $t('sendAGift') }}
</h2>
</div>
<!-- user avatar -->
<div
v-if="userReceivingGift"
class="modal-body"
>
<avatar
:member="userReceivingGift"
:hideClassBadge="true"
class="d-flex flex-column mx-auto align-items-center"
/>
<div class="avatar-spacer"></div>
<div class="d-flex flex-column mx-auto align-items-center display-name">
<!-- user display name and username -->
<user-link
:user-id="displayName"
:name="displayName"
:backer="userBacker"
:contributor="userContributor"
:class="display-name"
/>
</div>
<div class="d-flex flex-column mx-auto align-items-center user-name">
@{{ userName }}
</div>
</div>
<!-- menu area -->
<div class="row">
<div class="col-md-8 offset-md-2 text-center nav">
<div
class="nav-link"
:class="{active: selectedPage === 'subscription'}"
@click="selectPage('subscription')"
>
{{ $t('subscription') }}
</div>
<div
class="nav-link"
:class="{active: selectedPage !== 'subscription'}"
@click="selectPage('buyGems')"
>
{{ $t('gems') }}
</div>
</div>
</div>
<!-- subscriber block -->
<subscription-options
v-show="selectedPage === 'subscription'"
class="subscribe-option"
:userReceivingGift="userReceivingGift"
/>
<!-- gem block -->
<div
v-show="selectedPage === 'buyGems'"
>
<div class="gem-group">
<!-- buy gems with money -->
<label v-once>
{{ $t('howManyGemsPurchase') }}
</label>
<div class="d-flex flex-row align-items-center justify-content-center">
<div
class="gray-circle"
@click="gift.gems.amount <= 0
? gift.gems.amount = 0
: gift.gems.amount--"
>
<div
class="icon-negative"
v-html="icons.negativeIcon"
></div>
</div>
<div class="input-group">
<div class="input-group-prepend input-group-icon align-items-center">
<div
class="icon-gem"
v-html="icons.gemIcon"
></div>
</div>
<input
id="gemsForm"
v-model.number="gift.gems.amount"
class="form-control"
max="9999"
>
</div>
<div
class="gray-circle"
@click="gift.gems.amount++"
>
<div
class="icon-positive"
v-html="icons.positiveIcon"
></div>
</div>
</div>
<!-- the word "total" -->
<div class="buy-gem-total">
{{ $t('sendGiftTotal') }}
</div>
<!-- the actual dollar amount -->
<div class="buy-gem-amount">
<span>
{{ formatter.format(totalGems) }}
</span>
</div>
<!-- change to sending own gems page -->
<div
:class="{active: selectedPage === 'ownGems'}"
class="gem-state-change"
@click="selectPage('ownGems')"
>
{{ $t('wantToSendOwnGems') }}
</div>
</div>
<!-- paying for gems -->
<payments-buttons
class="payment-buttons"
:stripe-fn="() => redirectToStripe({gift, uuid: userReceivingGift._id, receiverName})"
:paypal-fn="() => openPaypalGift({
gift: gift, giftedTo: userReceivingGift._id, receiverName,
})"
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}"
/>
</div>
</div>
<!-- send gems from balance -->
<div
v-show="selectedPage === 'ownGems'"
>
<div class="gem-group">
<label v-once>
{{ $t('howManyGemsSend') }}
</label>
<div class="d-flex align-items-center justify-content-center">
<div
class="gray-circle"
@click="gift.gems.amount <= 0
? gift.gems.amount = 0
: gift.gems.amount--"
>
<div
class="icon-negative"
v-html="icons.negativeIcon"
></div>
</div>
<div class="input-group">
<div class="input-group-prepend input-group-icon align-items-center">
<div
class="icon-gem"
v-html="icons.gemIcon"
></div>
</div>
<input
id="gemsForm"
v-model="gift.gems.amount"
class="form-control"
:max="maxGems"
>
</div>
<div
class="gray-circle"
@click="gift.gems.amount < maxGems
? gift.gems.amount++
: gift.gems.amount = maxGems"
>
<div
class="icon-positive"
v-html="icons.positiveIcon"
></div>
</div>
</div>
<div class="align-items-middle">
<div class="d-flex justify-content-center align-items-middle">
<span class="balance-text">
{{ $t('yourBalance') }}
</span>
<span
class="icon-gem balance-gem-margin"
style="display: inline-block;"
v-html="icons.gemIcon"
></span>
<span
class="balance-gems"
>
{{ maxGems }}
</span>
</div>
</div>
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
<button
v-if="fromBal"
class="btn btn-primary mx-auto mt-2"
type="submit"
:disabled="sendingInProgress"
@click="sendGift()"
>
{{ $t("send") }}
</button>
</div>
<!-- change to buying gems page -->
<div
:class="{active: selectedPage === 'buyGems'}"
class="gem-state-change"
@click="selectPage('buyGems')"
>
{{ $t('needToPurchaseGems') }}
</div>
</div>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
#send-gift {
.modal-dialog {
max-width: 448px;
}
.modal-content {
width: 448px;
border-radius: 8px;
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
}
.modal-body{
padding: 0px;
}
.modal-close {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
.icon-close {
width: 18px;
height: 18px;
vertical-align: middle;
& ::v-deep svg path {
fill: #878190;
}
& :hover {
fill: #686274;
}
}
}
#subscription-form .subscribe-option {
background: #F9F9F9;
}
#subscription-form .selected {
background: rgba(213, 200, 255, 0.32);
// using rgba for transparency
}
}
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
h2 {
color: $purple-300;
padding-top: 2rem;
}
.avatar-spacer {
height: 9px;
}
.display-name {
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
margin: 0px 6px 0 20px;
}
.display-name a:hover{
text-decoration: none;
}
.user-name {
font-size: 0.75rem;
line-height: 1.33;
text-align: center;
color: $gray-100;
padding-bottom: 16px;
}
.row {
background-color: $gray-700;
margin: 0 0 0 0;
min-height: 32px;
}
.nav {
font-weight: bold;
font-size: 0.75rem;
min-height: 32px;
padding: 16px 0 0 0;
justify-content: center;
}
.nav-link {
color: $gray-100;
display: inline-block;
padding: 0px 8px 6px 8px;
&.active {
color: $purple-300;
border-bottom: 2px solid $purple-400;
}
&:hover {
color: $purple-300;
border-bottom: 2px solid $purple-400;
cursor: pointer;
}
}
.gem-group {
padding: 0 0 24px 0;
background-color: $gray-700;
margin: 0 0 0 0;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px
}
label {
color: $gray-50;
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
margin: 12px 0 16px 0;
width: 100%;
text-align: center;
}
.input-group {
width: 94px;
height: 32px;
margin: 0px 16px 0px 16px;
padding: 0;
border-radius: 2px;
border: solid 1px $gray-400;
background-color: $white;
}
.gray-circle {
border-radius: 100%;
border: solid 2px $gray-300;
width: 32px;
height: 32px;
cursor: pointer;
&:hover {
border-color: $purple-400;
}
}
.gray-circle:hover{
.icon-positive, .icon-negative {
& ::v-deep svg path {
fill: $purple-400;
}
}
}
.icon-gem {
width: 16px;
height: 16px;
margin-bottom: 4px;
}
.icon-positive, .icon-negative {
width: 10px;
height: 10px;
margin: 4px auto;
& ::v-deep svg path {
fill: $gray-300;
}
}
.buy-gem-total {
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
padding-top: 24px;
text-align: center;
height: 28px;
}
.buy-gem-amount {
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
margin: 16px 0 24px 0;
text-align: center;
height: 28px;
color: $green-10;
}
.balance-text {
font-size: 0.75rem;
font-weight: bold;
color: $gray-100;
line-height: 1.33;
margin: 12px 0px 0px 70px;
}
.balance-gem-margin {
margin: 8px 4px 0px 8px;
}
.balance-gems {
font-size: 0.75rem;
color: $gray-100;
line-height: 1.33;
margin: 12px 71px 0px 4px;
}
.gem-state-change {
color: $blue-10;
font-size: 0.875rem;
min-height: 24px;
margin: 16px 0 0;
text-align: center;
cursor: pointer;
}
.subscribe-option {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding-bottom: 24px;
}
.payment-buttons {
padding: 24px 0;
}
</style>
<script>
// libs imports
import { mapState } from '@/libs/store';
// mixins imports
import paymentsMixin from '../../mixins/payments';
// component imports
import avatar from '../avatar';
import userLink from '../userLink';
import subscriptionOptions from '../settings/subscriptionOptions.vue';
import paymentsButtons from '@/components/payments/buttons/list';
// svg imports
import closeIcon from '@/assets/svg/close.svg';
import gemIcon from '@/assets/svg/gem.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
export default {
components: {
avatar,
subscriptionOptions,
paymentsButtons,
userLink,
},
mixins: [
paymentsMixin,
],
data () {
return {
subscription: {
key: '',
},
icons: Object.freeze({
closeIcon,
gemIcon,
positiveIcon,
negativeIcon,
}),
userReceivingGift: {
profile: '',
},
name: '',
display: '',
selectedPage: 'subscription',
gift: {
type: 'gems',
gems: {
amount: 0,
fromBalance: true,
},
},
sendingInProgress: false,
amazonPayments: {},
gemCost: 1,
};
},
computed: {
...mapState({
userLoggedIn: 'user.data',
}),
userName () {
const userName = this.userReceivingGift.auth
&& this.userReceivingGift.auth.local
&& this.userReceivingGift.auth.local.username;
return userName;
},
displayName () {
const displayName = this.userReceivingGift.profile.name;
return displayName;
},
userBacker () {
const userBacker = this.userReceivingGift.backer;
return userBacker;
},
userContributor () {
const userContributor = this.userReceivingGift.contributor;
return userContributor;
},
tierIcon () {
if (this.isNPC) {
return this.icons.tierNPC;
}
return this.icons[`tier${this.level}`];
},
fromBal () {
return this.gift.type === 'gems' && this.gift.gems.fromBalance;
},
maxGems () {
const maxGems = this.fromBal ? this.userLoggedIn.balance * 4 : 9999;
return maxGems;
},
formatter () {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
});
return formatter;
},
totalGems () {
const totalGems = this.gift.gems.amount * 0.25;
return totalGems;
},
receiverName () {
if (
this.userReceivingGift.auth
&& this.userReceivingGift.auth.local
&& this.userReceivingGift.auth.local.username
) {
return this.userReceivingGift.auth.local.username;
}
return this.userReceivingGift.profile.name;
},
},
watch: {
startingPage () {
this.selectedPage = this.startingPage;
},
},
mounted () {
this.$root.$on('habitica::send-gift', data => {
this.userReceivingGift = data;
if (this.$store.state.giftModalOptions.startingPage) {
this.selectedPage = this.$store.state.giftModalOptions.startingPage;
this.$store.state.giftModalOptions.startingPage = '';
this.selectPage(this.selectedPage);
} else {
this.selectPage(this.startingPage);
}
this.setGemDefaults();
this.$root.$emit('bv::show::modal', 'send-gift');
});
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'send-gift');
},
setGemDefaults () {
if (this.selectedPage === 'buyGems') {
this.gift.gems.amount = 20;
} else if (this.selectedPage === 'ownGems') {
this.gift.gems.amount = 1;
} else {
this.gift.gems.amount = 0;
}
},
selectPage (page) {
if (page === this.selectedPage) return;
if (page === 'buyGems') this.selectedPage = 'buyGems';
if (page === 'buyGems' && this.selectedPage === 'ownGems') return;
this.selectedPage = page || 'subscription';
this.setGemDefaults();
},
async sendGift () {
this.sendingInProgress = true;
await this.$store.dispatch('members:transferGems', {
toUserId: this.userReceivingGift._id,
gemAmount: this.gift.gems.amount,
});
this.close();
setTimeout(() => { // wait for the send gem modal to be closed
this.$root.$emit('habitica:payment-success', {
paymentMethod: 'balance',
paymentCompleted: true,
paymentType: 'gift-gems-balance',
gift: {
gems: {
amount: this.gift.gems.amount,
},
},
giftReceiver: this.receiverName,
});
}, 500);
},
onHide () {
this.sendingInProgress = false;
},
},
};
</script>
@@ -739,6 +739,14 @@ export default {
} else if (attribute === 'email') {
this.user.auth.local.email = updates.newEmail;
window.alert(this.$t('emailSuccess')); // eslint-disable-line no-alert
} else if (attribute === 'password') {
this.passwordUpdates = {};
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('passwordSuccess'),
type: 'success',
timeout: true,
});
}
},
async changeDisplayName (newName) {
@@ -450,10 +450,6 @@
background-color: $white;
}
.subscribe-option {
border-bottom: 1px solid $gray-600;
}
.svg-amazon-pay {
width: 208px;
}
@@ -10,10 +10,17 @@
:value="block.key"
class="subscribe-option pt-2 pl-5 pb-3 mb-0"
:class="{selected: subscription.key === block.key}"
@click.native="subscription.key = block.key"
@click.native="updateSubscriptionData(block.key)"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div
v-if="userReceivingGift && userReceivingGift._id"
class="subscription-text ml-2 mb-1"
v-html="$t('giftSubscriptionRateText', {price: block.price, months: block.months})"
>
</div>
<div
v-else
class="subscription-text ml-2 mb-1"
v-html="$t('subscriptionRateText', {price: block.price, months: block.months})"
>
@@ -25,7 +32,18 @@
</div>
</b-form-radio>
</b-form-group>
<!-- payment buttons first is for gift subs and the second is for renewing subs -->
<payments-buttons
v-if="userReceivingGift && userReceivingGift._id"
:disabled="!subscription.key"
:stripe-fn="() => redirectToStripe({gift, uuid: userReceivingGift._id, receiverName})"
:paypal-fn="() => openPaypalGift({
gift: gift, giftedTo: userReceivingGift._id, receiverName,
})"
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}"
/>
<payments-buttons
v-else
:disabled="!subscription.key"
:stripe-fn="() => redirectToStripe({
subscription: subscription.key,
@@ -43,6 +61,7 @@
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#subscription-form {
.custom-control .custom-control-label::before,
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
@@ -101,11 +120,22 @@ export default {
mixins: [
paymentsMixin,
],
props: {
userReceivingGift: {
type: Object,
default () {},
},
},
data () {
return {
subscription: {
key: null,
key: 'basic_earned',
},
gift: {
type: 'subscription',
subscription: { key: 'basic_earned' },
},
receiverName: '',
};
},
computed: {
@@ -114,7 +144,6 @@ export default {
},
subscriptionBlocksOrdered () {
const subscriptions = filter(subscriptionBlocks, o => o.discount !== true);
return sortBy(subscriptions, [o => o.months]);
},
},
@@ -131,6 +160,10 @@ export default {
return '<span class="subscription-bubble px-2 py-1">Gem cap at 25</span>';
}
},
updateSubscriptionData (key) {
this.subscription.key = key;
if (this.userReceivingGift._id) this.gift.subscription.key = key;
},
},
};
</script>
@@ -209,6 +209,7 @@
<li>Fox_town</li>
<li>MaybeSteveRogers</li>
<li>shanaqui</li>
<li>deilann (not yet pictured)</li>
</ul>
</div>
</div>
@@ -218,7 +219,7 @@
{{ $t('commGuidePara014') }}<br>
<em>
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
deilann, Breadstrings, Megan, Blade, and Daniel the Bard
Breadstrings, Megan, Blade, and Daniel the Bard
</em>
</p>
<h2 id="final">
+1 -5
View File
@@ -94,6 +94,7 @@ export default {
'gems',
'bugs-features',
'world-boss',
'group-plans',
];
const hash = window.location.hash.replace('#', '');
@@ -105,11 +106,6 @@ export default {
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
},
visible: hash && headings.includes(hash) ? hash : null,
// @TODO webFaqStillNeedHelp: {
// linkStart: '[',
// linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)',
// },
// "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the <%= linkStart %>Habitica Help guild<%= linkEnd %>! We're happy to help."
};
},
mounted () {
@@ -1,59 +1,117 @@
<template>
<div>
<approval-modal :task="task" />
<div
class="gray-100"
>
<div
v-if="showStatus"
>
<div
v-for="completion in completionsList"
:key="completion.userId"
class="d-flex completion-row"
>
<div class="control">
<div
class="inner d-flex justify-content-center align-items-center p-auto"
:class="{interactive: iconClass(completion) !== 'lock'}"
@click="iconClass(completion) !== 'lock' ? needsWork(completion) : null"
>
<div
class="icon"
:class="iconClass(completion)"
v-html="icons[iconClass(completion)]"
>
</div>
</div>
</div>
<div
class="px-75 py-2 info"
>
<div>
<strong> @{{ completion.userName }} </strong>
</div>
<div
v-if='completion.completedDate'
:class="{'green-10': completion.completed}"
>
{{ completion.completedDateString }}
</div>
</div>
</div>
</div>
<div
v-if="!(userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit')"
class="claim-bottom-message d-flex align-items-center"
>
<div
class="mr-auto ml-2"
v-if="assignedUsersCount > 0"
class="svg-icon ml-2 users-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.users"
></div>
<div
class="mr-auto ml-1 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="message"
></div>
<div
v-if="!userIsAssigned && !task.completed"
class="ml-auto mr-2"
class="d-flex ml-auto mr-1 my-auto"
v-if="task.group.assignedUsers && ['daily','todo'].indexOf(task.type) !== -1"
>
<a
class="claim-color"
@click="claim()"
>{{ $t('claim') }}</a>
<span
v-if="assignedUsersCount > 1"
class="d-flex mr-1 my-auto"
>
<span
class="small-check"
v-if="!showStatus && completionsCount"
>
<div
class="svg-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.check"
>
</div>
</span>
<span
class="ml-1 mr-2 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-if="!showStatus && completionsCount"
>
{{ completionsCount }}/{{ assignedUsersCount }}
</span>
<a
v-if="assignedUsersCount > 1 && !showStatus"
class="blue-10"
@click="showStatus = !showStatus"
>
{{ $t('viewStatus') }}
</a>
<a
v-if="showStatus"
@click="showStatus = !showStatus"
>
{{ $t('close') }}
</a>
</span>
<span
v-if="assignedUsersCount === 1 && task.type === 'daily'
&& !task.completed && singleAssignLastDone"
class="mr-1 d-inline-flex"
>
<span
v-html="icons.lastComplete"
v-b-tooltip.hover.bottom="$t('lastCompleted')"
class="svg-icon color last-completed mr-1 my-auto"
:class="{'gray-200': completionsCount !== assignedUsersCount}"
>
</span>
<span
:class="{'green-10': completionsCount === assignedUsersCount}"
>
{{ formattedCompletionTime }}
</span>
</span>
</div>
<div
v-if="userIsAssigned && !approvalRequested && !task.completed"
class="ml-auto mr-2"
>
<a
class="unclaim-color"
@click="unassign()"
>{{ $t('removeClaim') }}</a>
</div>
</div>
<div
v-if="approvalRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="approve()"
>{{ $t('approveTask') }}</a>
<a @click="needsWork()">{{ $t('needsWork') }}</a>
</div>
<div
v-if="multipleApprovalsRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center"
>
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
</div>
<div
v-if="userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit'"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="$emit('claimRewards')"
>{{ $t('claimRewards') }}</a>
</div>
</div>
</template>
@@ -61,148 +119,220 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.claim-bottom-message {
background-color: $gray-700;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
color: $gray-200;
background-color: $gray-600;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
font-size: 12px;
line-height: 1.334;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
z-index: 9;
height: 24px;
.blue-10 {
color: $blue-10;
}
}
.approve-color {
color: $green-10 !important;
.completion-row {
height: 3rem;
background-color: $gray-700;
font-size: 12px;
.control {
background-color: $gray-200;
border-top: 1px solid $gray-100;
width: 40px;
height: 48px;
padding: 9px 6px;
.inner {
width: 28px;
height: 28px;
padding: 6px;
border-radius: 2px;
background-color: rgba($white, 0.5);
cursor: default;
&.interactive {
cursor: pointer;
&:hover {
background-color: rgba($white, 0.75);
}
}
.icon {
color: $gray-10;
&.lock {
width: 10px;
}
&.check {
margin-left: 2px;
width: 12px;
}
}
}
}
.info {
width: 100%;
border-top: 1px solid $gray-600;
}
}
.claim-color {
color: $blue-10 !important;
.gray-100 {
color: $gray-100;
}
.unclaim-color {
color: $red-50 !important;
.green-10 {
color: $green-10;
}
.last-completed {
width: 16px;
height: 14px;
margin-bottom: 4px;
}
.small-check {
display: inline-flex;
width: 16px;
height: 16px;
border-radius: 2px;
background-color: $gray-500;
.svg-icon {
width: 8px;
height: 6px;
margin: auto;
}
}
.users-icon {
width: 16px;
}
</style>
<script>
import findIndex from 'lodash/findIndex';
import moment from 'moment';
import reduce from 'lodash/reduce';
import { mapState } from '@/libs/store';
import approvalModal from './approvalModal';
import sync from '@/mixins/sync';
import checkIcon from '@/assets/svg/check.svg';
import lockIcon from '@/assets/svg/lock.svg';
import usersIcon from '@/assets/svg/users.svg';
import lastComplete from '@/assets/svg/last-complete.svg';
export default {
components: {
approvalModal,
},
mixins: [sync],
props: ['task', 'group'],
data () {
return {
showStatus: false,
icons: Object.freeze({
check: checkIcon,
lastComplete,
lock: lockIcon,
users: usersIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
userIsAssigned () {
return this.task.group.assignedUsers
&& this.task.group.assignedUsers.indexOf(this.user._id) !== -1;
},
message () {
const { assignedUsers } = this.task.group;
const assignedUsersNames = [];
const assignedUsersLength = assignedUsers.length;
// @TODO: Eh, I think we only ever display one user name
if (this.group && this.group.members) {
assignedUsers.forEach(userId => {
const index = findIndex(this.group.members, member => member._id === userId);
const assignedMember = this.group.members[index];
assignedUsersNames.push(`@${assignedMember.auth.local.username}`);
});
}
if (assignedUsersLength === 1 && !this.userIsAssigned) {
return this.$t('assignedToUser', { userName: assignedUsersNames[0] });
} if (assignedUsersLength > 1 && !this.userIsAssigned) {
return this.$t('assignedToMembers', { userCount: assignedUsersLength });
} if (assignedUsersLength > 1 && this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: assignedUsersLength - 1 });
} if (this.userIsAssigned) {
return this.$t('youAreAssigned');
} // if (assignedUsersLength === 0) {
return this.$t('taskIsUnassigned');
return this.task.group.assignedUsersDetail
&& Boolean(this.task.group.assignedUsersDetail[this.user._id]);
},
userIsManager () {
if (
this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id])
) return true;
return false;
return this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id]);
},
approvalRequested () {
if (
(this.task.approvals && this.task.approvals.length === 1)
|| (this.task.group && this.task.group.approval && this.task.group.approval.requested)
) {
return true;
assignedUsersCount () {
return this.task.group.assignedUsers.length;
},
completionsCount () {
return reduce(this.task.group.assignedUsersDetail, (count, assignment) => {
if (assignment.completed) return count + 1;
return count;
}, 0);
},
completionsList () {
if (this.assignedUsersCount === 1) return [];
const completionsArray = [];
for (const userId of this.task.group.assignedUsers) {
if (userId !== this.user._id) {
const { completedDate } = this.task.group.assignedUsersDetail[userId];
let completedDateString;
if (moment().diff(completedDate, 'days') > 0) {
completedDateString = `Completed ${moment(completedDate).format('M/D/YY')}`;
} else {
completedDateString = `Completed at ${moment(completedDate).format('h:mm A')}`;
}
completionsArray.push({
userId,
userName: this.task.group.assignedUsersDetail[userId].assignedUsername,
completed: this.task.group.assignedUsersDetail[userId].completed,
completedDate,
completedDateString,
});
}
}
return false;
return completionsArray;
},
multipleApprovalsRequested () {
if (this.task.approvals && this.task.approvals.length > 1) return true;
return false;
message () {
if (this.assignedUsersCount > 1) { // Multi assigned
if (this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: this.assignedUsersCount - 1 });
}
return this.$t('assignedToMembers', { userCount: this.assignedUsersCount });
}
if (this.assignedUsersCount === 1) { // Singly assigned
const userId = this.task.group.assignedUsers[0];
const userName = this.task.group.assignedUsersDetail[userId].assignedUsername;
if (this.task.group.assignedUsersDetail[userId].completed) { // completed
const { completedDate } = this.task.group.assignedUsersDetail[userId];
if (this.userIsAssigned) {
if (moment().diff(completedDate, 'days') > 0) {
return `<strong>You</strong> completed ${moment(completedDate).format('M/D/YY')}`;
}
return `<strong>You</strong> completed at ${moment(completedDate).format('h:mm A')}`;
}
if (moment().diff(completedDate, 'days') > 0) {
return `@${userName} completed ${moment(completedDate).format('M/D/YY')}`;
}
return `@${userName} completed at ${moment(completedDate).format('h:mm A')}`;
}
if (this.userIsAssigned) {
return this.$t('youEmphasized');
}
return `@${userName}`;
}
return this.$t('error'); // task is open, or the other conditions aren't hitting right
},
singleAssignLastDone () {
const completion = this.task?.group?.assignedUsersDetail[this.user._id];
if (completion) return completion.completedDate;
return null;
},
formattedCompletionTime () {
if (!this.singleAssignLastDone) return '';
if (moment().diff(this.singleAssignLastDone, 'days') < 1) {
return moment(this.singleAssignLastDone).format('h:mm A');
}
return moment(this.singleAssignLastDone).format('M/D/YY');
},
},
methods: {
async claim () {
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:assignTask', {
taskId,
userId: this.user._id,
});
this.task.group.assignedUsers.push(this.user._id);
this.sync();
iconClass (completion) {
if (this.userIsManager && completion.completed) return 'check';
return 'lock';
},
async unassign () {
if (!window.confirm(this.$t('confirmUnClaim'))) return; // eslint-disable-line no-alert
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:unassignTask', {
taskId,
userId: this.user._id,
});
const index = this.task.group.assignedUsers.indexOf(this.user._id);
this.task.group.assignedUsers.splice(index, 1);
this.sync();
},
approve () {
const userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(0, 1);
this.task.approvals.splice(0, 1);
this.sync();
},
needsWork () {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[0];
needsWork (completion) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
userId: completion.userId,
});
this.task.approvals.splice(0, 1);
this.sync();
},
showRequests () {
this.$root.$emit('bv::show::modal', 'approval-modal');
this.task.group.assignedUsersDetail[completion.userId].completed = false;
},
},
};
@@ -1,80 +0,0 @@
<template>
<b-modal
id="approval-modal"
:title="$t('approveTask')"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
v-for="(approval, index) in task.approvals"
class="row approval"
>
<div class="col-8">
<strong>{{ approval.userId.profile.name }}</strong>
</div>
<div class="col-2">
<button
class="btn btn-primary"
@click="approve(index)"
>
{{ $t('approve') }}
</button>
</div>
<div class="col-2">
<button
class="btn btn-secondary"
@click="needsWork(index)"
>
{{ $t('needsWork') }}
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('close') }}
</button>
</div>
</b-modal>
</template>
<style scoped>
.row.approval {
padding-top: 1em;
padding-bottom: 1em;
}
</style>
<script>
export default {
props: ['task'],
methods: {
approve (index) {
const userIdToApprove = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(index, 1);
this.task.approvals.splice(index, 1);
},
needsWork (index) {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(index, 1);
},
close () {
this.$root.$emit('bv::hide::modal', 'approval-modal');
},
},
};
</script>
+38 -15
View File
@@ -43,7 +43,7 @@
class="tasks-list"
>
<textarea
v-if="isUser"
v-if="isUser || canCreateTasks()"
ref="quickAdd"
v-model="quickAddText"
class="quick-add"
@@ -102,6 +102,7 @@
:group="group"
:challenge="challenge"
@editTask="editTask"
@taskSummary="taskSummary"
@moveTo="moveTo"
@taskDestroyed="taskDestroyed"
/>
@@ -347,18 +348,19 @@
import throttle from 'lodash/throttle';
import isEmpty from 'lodash/isEmpty';
import draggable from 'vuedraggable';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import Task from './task';
import ClearCompletedTodos from './clearCompletedTodos';
import buyMixin from '@/mixins/buy';
import sync from '@/mixins/sync';
import { mapState, mapActions, mapGetters } from '@/libs/store';
import shopItem from '../shops/shopItem';
import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue';
import PinBadge from '@/components/ui/pinBadge';
import notifications from '@/mixins/notifications';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import {
getTypeLabel,
@@ -382,7 +384,7 @@ export default {
shopItem,
draggable,
},
mixins: [buyMixin, notifications],
mixins: [buyMixin, notifications, sync],
// @TODO Set default values for props
// allows for better control of props values
// allows for better control of where this component is called
@@ -542,6 +544,7 @@ export default {
...mapActions({
loadCompletedTodos: 'tasks:fetchCompletedTodos',
createTask: 'tasks:create',
createGroupTasks: 'tasks:createGroupTasks',
}),
async taskSorted (data) {
const filteredList = this.taskList;
@@ -574,18 +577,25 @@ export default {
},
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
const list = this.getUnfilteredTaskList(this.type);
const list = this.taskListOverride || this.getUnfilteredTaskList(this.type);
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
const moved = list.splice(oldPosition, 1);
const newPosition = where === 'top' ? 0 : list.length;
list.splice(newPosition, 0, moved[0]);
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
if (!this.isUser) {
await this.$store.dispatch('tasks:moveGroupTask', {
taskId: taskIdToMove,
position: newPosition,
});
} else {
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
}
},
async rewardSorted (data) {
const rewardsList = this.inAppRewards;
@@ -606,7 +616,12 @@ export default {
this.showPopovers = true;
this.isDragging(false);
},
quickAdd (ev) {
canCreateTasks () {
if (!this.group) return false;
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
async quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
if (ev.shiftKey) {
this.quickAddRows += 1;
@@ -620,19 +635,27 @@ export default {
const tasks = text.split('\n').reverse().filter(taskText => (!!taskText)).map(taskText => {
const task = taskDefaults({ type: this.type, text: taskText }, this.user);
task.tags = this.selectedTags.slice();
if (this.isUser) task.tags = this.selectedTags.slice();
return task;
});
this.quickAddText = '';
this.quickAddRows = 1;
this.createTask(tasks);
if (this.group) {
await this.createGroupTasks({ groupId: this.group.id, tasks });
this.sync();
} else {
this.createTask(tasks);
}
this.$refs.quickAdd.blur();
return true;
},
editTask (task) {
this.$emit('editTask', task);
},
taskSummary (task) {
this.$emit('taskSummary', task);
},
activateFilter (type, filter = '') {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
@@ -687,7 +710,7 @@ export default {
filterByLabel (taskList, type, filter) {
if (!taskList) return [];
const selectedFilter = getActiveFilter(type, filter, this.challenge);
return sortAndFilterTasks(taskList, selectedFilter);
return sortAndFilterTasks(taskList, selectedFilter, Boolean(this.group));
},
filterByTagList (taskList, tagList = []) {
let filteredTaskList = taskList;
@@ -1,80 +1,98 @@
<template>
<div class="checklist-component">
<lockable-label
:locked="disabled || disableItems"
:text="$t('checklist')"
/>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
<div
class="d-flex"
>
<lockable-label
:locked="disabled"
:text="$t('checklist')"
/>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
class="svg-icon icon-16 my-auto ml-auto pointer"
:class="{'chevron-flip': showChecklist}"
v-html="icons.chevron"
@click="showChecklist = !showChecklist"
>
</div>
</div>
<b-collapse
id="checklistCollapse"
v-model="showChecklist"
>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
>
</span>
<checkbox
v-if="!disableEdit"
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled"
class="input-group-prepend"
:class="{'cursor-auto': disabled}"
/>
<input
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableEdit"
:class="summaryClass(item)"
>
<span
v-if="!disabled && !disableEdit"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableEdit"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<checkbox
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled || disableItems"
class="input-group-prepend"
:class="{'cursor-auto': disabled || disableItems}"
/>
<input
v-model="item.text"
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableItems"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
<span
v-if="!disabled && !disableItems"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableItems"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<input
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
</div>
</b-collapse>
</div>
</template>
@@ -104,7 +122,7 @@ export default {
disableDrag: {
type: Boolean,
},
disableItems: {
disableEdit: {
type: Boolean,
},
items: {
@@ -114,6 +132,7 @@ export default {
data () {
return {
checklist: this.items,
showChecklist: true,
hasPossibilityOfIMEConversion: true,
newChecklistItem: null,
icons: Object.freeze({
@@ -125,6 +144,11 @@ export default {
};
},
methods: {
summaryClass (item) {
if (!this.disableEdit) return '';
if (item.completed) return 'summary-completed';
return 'summary-incomplete';
},
updateChecklist () {
this.$emit('update:items', this.checklist);
},
@@ -166,14 +190,22 @@ export default {
.checklist-component {
.top-border {
border-top: 1px solid $gray-500;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
}
.lock-icon {
color: $gray-200;
}
.pointer {
cursor: pointer;
}
.top-border {
border-top: 1px solid $gray-500;
}
.checklist-group {
height: 2rem;
border-bottom: 1px solid $gray-500;
@@ -251,6 +283,13 @@ export default {
border-radius: 0px;
border: none !important;
padding-left: 36px;
&.summary-incomplete {
opacity: 1;
}
&.summary-completed {
text-decoration: line-through;
}
}
.checklist-group {
@@ -4,16 +4,16 @@
:class="{ 'break': maxItems === 0 }"
>
<template v-if="items.length === 0">
<div class="items-none">
<div class="items-none mb-1">
{{ emptyMessage }}
</div>
</template>
<template v-else>
<div
v-for="item in truncatedSelectedItems"
v-for="item in items"
:key="item.id"
:title="item.name"
class="multi-item mr-1 d-inline-flex align-items-center"
class="multi-item mr-1 mb-1 d-inline-flex align-items-center"
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
@click.stop="removeItem($event, item)"
@@ -27,12 +27,6 @@
v-html="icons.remove"
></div>
</div>
<div
v-if="remainingSelectedItems.length > 0"
class="items-more ml-75"
>
+{{ remainingSelectedItems.length }}
</div>
</template>
</div>
</template>
@@ -1,4 +1,4 @@
multi<template>
<template>
<div>
<b-dropdown
ref="dropdown"
@@ -29,6 +29,7 @@ multi<template>
</b-dropdown-header>
<template v-slot:button-content>
<multi-list
class="d-flex flex-wrap"
:items="selectedItemsAsObjects"
:add-new="addNew"
:pill-invert="pillInvert"
@@ -85,6 +86,13 @@ multi<template>
$itemHeight: 2rem;
.inline-dropdown {
&.select-multi .dropdown-toggle {
height: auto;
padding-bottom: 0px;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
@@ -0,0 +1,294 @@
<template>
<div>
<b-dropdown
ref="dropdown"
class="inline-dropdown select-multi"
:toggle-class="isOpened ? 'active' : null"
:class="{'margin-adjust': selectedItem}"
@show="wasOpened()"
@hide="hideCallback($event)"
@toggle="openOrClose($event)"
>
<b-dropdown-header>
<div class="mb-2">
<b-form-input
v-model="search"
type="text"
:placeholder="searchPlaceholder"
@keyup.enter="handleSubmit"
/>
</div>
</b-dropdown-header>
<template v-slot:button-content>
<div
class="mr-1 d-inline-flex align-items-center"
@click.stop="selectItem({id: selectedItem})"
v-markdown="
allItemsMap[selectedItem] ? `@${allItemsMap[selectedItem].name}`
: emptyMessage
"
>
</div>
</template>
<div
v-if="addNew || availableToSelect.length > 0"
:class="{
'item-group': true,
'add-new': availableToSelect.length === 0 && search !== '',
'scroll': availableToSelect.length > 5
}"
>
<b-dropdown-item-button
v-for="item in availableToSelect"
:key="item.id"
class="ignore-hide multi-item"
:class="{ 'none': item.id === 'none', selectListItem: true }"
@click.prevent.stop="selectItem(item)"
>
<div
v-markdown="item.name"
class="label"
></div>
<div
v-if="item.addlText"
class="addl-text"
>
{{ item.addlText }}
</div>
</b-dropdown-item-button>
</div>
</b-dropdown>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
$itemHeight: 2rem;
.selected-item {
display: inline-block;
height: 1.5rem;
border-radius: 100px;
background-color: $white;
border: solid 1px $gray-400;
position: relative;
top: -1px;
.multi-label {
height: 1rem;
font-size: 12px;
line-height: 16px;
letter-spacing: normal;
color: $gray-100;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
}
.dropdown-header {
background-color: $gray-700;
padding-bottom: 0;
min-height: 3rem;
}
.dropdown-item, .dropdown-header {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.none {
cursor: default;
pointer-events: none;
}
.multi-item button {
height: $itemHeight;
display: flex;
.label {
height: 1.5rem;
font-size: 14px;
line-height: 1.71;
flex: 1;
}
.addl-text {
height: 1rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
text-align: right;
color: $gray-100;
align-self: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
margin-right: 0.25rem;
}
&:hover {
.addl-text {
color: $purple-300;
}
}
}
.item-group {
max-height: #{5*$itemHeight};
&.add-new {
height: 30px;
.hint {
display: block;
}
}
&.scroll {
overflow-y: scroll;
}
}
.hint {
display: none;
height: 2rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
color: $gray-100;
margin-left: 0.75rem;
margin-top: 0.5rem;
}
}
</style>
<script>
import Vue from 'vue';
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
components: {
},
props: {
addNew: {
type: Boolean,
default: false,
},
allItems: {
type: Array,
},
emptyMessage: {
type: String,
},
searchPlaceholder: {
type: String,
},
selectedItem: {
type: String,
},
},
data () {
return {
preventHide: true,
isOpened: false,
selected: this.selectedItem,
search: '',
};
},
computed: {
allItemsMap () {
const obj = {};
this.allItems.forEach(t => {
obj[t.id] = t;
});
return obj;
},
selectedItemAsObject () {
return this.selectedItem ? this.allItemsMap[this.selectedItem] : null;
},
availableToSelect () {
const searchString = this.search.toLowerCase();
const filteredItems = this.allItems.filter(i => i.name.toLowerCase().includes(searchString));
return filteredItems;
},
},
watch: {
selected () {
this.$emit('changed', this.selectedItem);
},
},
created () {
document.addEventListener('keyup', this.handleEsc);
},
beforeDestroy () {
document.removeEventListener('keyup', this.handleEsc);
},
mounted () {
this.$refs.dropdown.clickOutHandler = () => {
this.closeSelectPopup();
};
},
methods: {
closeSelectPopup () {
this.preventHide = false;
this.isOpened = false;
Vue.nextTick(() => {
this.$refs.dropdown.hide();
});
},
openOrClose ($event) {
if (this.isOpened) {
this.closeSelectPopup();
$event.preventDefault();
}
},
selectItem (item) {
if (item.id === this.selectedItem) {
this.$emit('toggle', null);
} else {
this.$emit('toggle', item.id);
}
this.closeSelectPopup();
},
hideCallback ($event) {
if (this.preventHide) {
$event.preventDefault();
return;
}
this.isOpened = false;
},
wasOpened () {
this.isOpened = true;
this.preventHide = true;
},
handleEsc (e) {
if (e.keyCode === 27) {
this.closeSelectPopup();
}
},
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
this.$emit('addNew', search);
this.search = '';
},
},
};
</script>
+15 -17
View File
@@ -68,7 +68,8 @@
</b-popover>
<div
class="spell-border"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl }"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl,
'insufficient-mana': user.stats.mp < skill.mana }"
>
<div
class="spell"
@@ -87,19 +88,6 @@
<div>Level {{ skill.lvl }}</div>
</div>
</div>
<div
v-else-if="spellDisabled(key) === true"
class="mana"
>
<div class="mana-text">
<div
v-once
class="svg-icon"
v-html="icons.mana"
></div>
<div>{{ skill.mana }}</div>
</div>
</div>
<div
v-else
class="mana"
@@ -200,7 +188,7 @@
border-radius: 4px;
margin-bottom: 1rem;
&:hover:not(.disabled) {
&:hover:not(.disabled):not(.insufficient-mana) {
background-color: $purple-400;
cursor: pointer;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16),
@@ -216,6 +204,10 @@
background-color: rgba(26, 24, 29, 0.5);
}
.mana-text {
color: $blue-500;
}
.level {
color: $white;
font-weight: normal;
@@ -223,6 +215,10 @@
}
}
&.insufficient-mana:not(.disabled) {
opacity: 0.5;
}
.spell {
background: $white;
border-radius: 4px;
@@ -465,7 +461,7 @@ export default {
spellDisabled (skill) {
const incompleteDailiesDue = this
.getUnfilteredTaskList('daily')
.filter(daily => !daily.completed && daily.isDue)
.filter(daily => !daily.completed && !daily.group.id && daily.isDue)
.length;
if (skill === 'frost' && this.user.stats.buffs.streaks) {
@@ -481,7 +477,9 @@ export default {
skillNotes (skill) {
let notes = skill.notes();
if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
if (this.user.stats.lvl < skill.lvl) {
notes = this.$t('spellLevelTooHigh', { level: skill.lvl });
} else if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
} else if (skill.key === 'stealth' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
+139 -84
View File
@@ -3,47 +3,39 @@
<div
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess},
`type_${task.type}`
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
>
<approval-header
v-if="task.group.id"
:task="task"
:group="group"
/>
<div
class="d-flex"
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
&& !(task.group.approval.approved && task.type === 'habit')}"
:class="{'task-not-scoreable': showTaskLockIcon }"
>
<!-- Habits left side control-->
<div
v-if="task.type === 'habit'"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass
}, controlClass.up.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-positive-enabled': task.up && isUser,
'habit-control-positive-disabled': !task.up && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.up.inner]"
tabindex="0"
@click="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@click="score('up')"
@keypress.enter="score('up')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.up ? controlClass.up.icon : 'positive'"
v-html="icons.lock"
@@ -60,20 +52,21 @@
v-if="task.type === 'daily' || task.type === 'todo'"
class="left-control d-flex justify-content-center"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.bg]"
>
<div
class="task-control daily-todo-control"
:class="controlClass.inner"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.inner,
]"
tabindex="0"
@click="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@keypress.enter="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="controlClass.icon"
v-html="icons.lock"
@@ -82,7 +75,7 @@
v-else
class="svg-icon check"
:class="{
'display-check-icon': task.completed || task.group.approval.requested,
'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
@@ -95,8 +88,8 @@
:class="contentClass"
>
<div
class="task-clickable-area"
:class="{ 'cursor-auto': !isUser && !teamManagerAccess }"
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
@@ -105,14 +98,14 @@
<h3
v-markdown="task.text"
class="task-title markdown"
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
:class="{ 'has-notes': task.notes }"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
ref="taskDropdown"
v-b-tooltip.hover.top="$t('options')"
tabindex="0"
class="task-dropdown"
class="task-dropdown mr-1"
:right="task.type === 'reward'"
>
<div slot="dropdown-toggle">
@@ -138,7 +131,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToTop"
@@ -153,7 +145,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToBottom"
@@ -186,7 +177,7 @@
</menu-dropdown>
</div>
<div
v-markdown="displayNotes"
v-markdown="task.notes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
@@ -220,7 +211,7 @@
v-if="!task.collapseChecklist"
:key="item.id"
class="custom-control custom-checkbox checklist-item"
:class="{'checklist-item-done': item.completed, 'cursor-auto': !isUser}"
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<input
@@ -229,14 +220,14 @@
tabindex="0"
type="checkbox"
:checked="item.completed"
:disabled="castingSpell || !isUser"
:disabled="castingSpell || showTaskLockIcon"
@change="toggleChecklistItem(item)"
@keypress.enter="toggleChecklistItem(item)"
>
<label
v-markdown="item.text"
class="custom-control-label"
:class="{ 'cursor-auto': !isUser }"
:class="{ 'cursor-auto': showTaskLockIcon }"
:for="`checklist-${item.id}-${random}`"
></label>
</div>
@@ -316,7 +307,7 @@
></div>
</div>
<div
v-if="hasTags"
v-if="hasTags && !task.group.id"
:id="`tags-icon-${task._id}`"
class="d-flex align-items-center"
>
@@ -326,7 +317,7 @@
></div>
</div>
<b-popover
v-if="hasTags"
v-if="hasTags && !task.group.id"
:target="`tags-icon-${task._id}`"
triggers="hover"
placement="bottom"
@@ -356,25 +347,22 @@
v-if="task.type === 'habit'"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.down.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-negative-enabled': task.down && isUser,
'habit-control-negative-disabled': !task.down && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.down.inner]"
tabindex="0"
@click="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.down ? controlClass.down.icon : 'negative'"
v-html="icons.lock"
@@ -390,13 +378,22 @@
<div
v-if="task.type === 'reward'"
class="right-control d-flex align-items-center justify-content-center reward-control"
:class="controlClass.bg"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.bg,
]"
tabindex="0"
@click="isUser ? score('down') : null"
@keypress.enter="isUser ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
class="svg-icon"
v-if="showTaskLockIcon"
class="svg-icon color lock"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon mb-1"
v-html="icons.gold"
></div>
<div class="small-text">
@@ -405,10 +402,9 @@
</div>
</div>
<approval-footer
v-if="task.group.id"
v-if="task.group.id && !isOpenTask"
:task="task"
:group="group"
@claimRewards="score('up')"
/>
</div>
</div>
@@ -427,7 +423,7 @@
border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 2px;
border-radius: 4px;
}
}
@@ -449,11 +445,11 @@
margin-bottom: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: white;
border-radius: 2px;
border-radius: 4px;
position: relative;
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
@@ -469,10 +465,10 @@
}
.task.groupTask {
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
border: $purple-400 solid 1px;
border-radius: 3px;
border-radius: 5px;
margin: -1px; // to counter the border width
margin-bottom: 1px;
transition: none; // with transition, the border color switches from black to $purple-400
@@ -495,6 +491,7 @@
}
&.has-notes {
margin-bottom: 0px;
padding-bottom: 4px;
}
@@ -508,8 +505,6 @@
}
.task-clickable-area {
padding: 7px 8px;
padding-bottom: 0px;
border: transparent solid 1px;
cursor: pointer;
@@ -518,7 +513,7 @@
}
&:focus {
border-radius: 2px;
border-radius: 4px;
border: $purple-400 solid 1px;
}
}
@@ -575,7 +570,11 @@
}
.task-dropdown {
max-height: 18px;
height: 16px;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.task-dropdown ::v-deep .dropdown-menu {
@@ -632,8 +631,8 @@
}
&.reward-content {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
@@ -796,8 +795,8 @@
}
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
min-height: 60px;
border: 1px solid transparent;
border-right: none;
@@ -809,15 +808,15 @@
.task:not(.type_habit) {
.left-control {
& + .task-content {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
min-height: 56px;
border: 1px solid transparent;
border-left: none;
@@ -853,8 +852,12 @@
height: 24px;
}
.lock {
color: $gray-200;
width: 15px;
}
.small-text {
margin-top: 4px;
font-style: initial;
font-weight: bold;
}
@@ -920,20 +923,19 @@ import lockIcon from '@/assets/svg/lock.svg';
import menuIcon from '@/assets/svg/menu.svg';
import markdownDirective from '@/directives/markdown';
import scoreTask from '@/mixins/scoreTask';
import approvalHeader from './approvalHeader';
import sync from '@/mixins/sync';
import approvalFooter from './approvalFooter';
import MenuDropdown from '../ui/customMenuDropdown';
export default {
components: {
approvalFooter,
approvalHeader,
MenuDropdown,
},
directives: {
markdown: markdownDirective,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
// @TODO: maybe we should store the group on state?
props: {
task: {},
@@ -1064,11 +1066,45 @@ export default {
},
teamManagerAccess () {
if (!this.isGroupTask || !this.group) return true;
if (!this.group.leader && !this.group.managers) return false;
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
},
displayNotes () {
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
return this.task.notes;
isOpenTask () {
if (!this.isGroupTask) return false;
if (this.task?.group?.assignedUsers?.length > 0) return false;
return true;
},
showCheckIcon () {
if (this.isGroupTask && this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]) {
return this.task.group.assignedUsersDetail[this.user._id].completed;
}
return this.task.completed;
},
showTaskLockIcon () {
if (this.isUser) return false;
if (this.isGroupTask) {
if (this.task.completed) {
if (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]
) {
return false;
}
if (this.task.group.completedBy.userId === this.user._id) return false;
if (this.teamManagerAccess) {
if (!this.task.group.assignedUsers || this.task.group.assignedUsers.length === 0) {
return false;
}
if (this.task.group.assignedUsers.length === 1) return false;
}
return true;
}
if (this.isOpenTask) return false;
if (this.task.group.assignedUsersDetail[this.user._id]) {
return false;
}
}
return true;
},
},
methods: {
@@ -1098,7 +1134,7 @@ export default {
return this.task.date && this.$t('dueIn', { dueIn });
},
edit (e, task) {
if (this.isRunningYesterdailies || !this.showEdit) return;
if (this.isRunningYesterdailies) return;
const target = e.target || e.srcElement;
/*
@@ -1116,8 +1152,11 @@ export default {
const isEditAction = this.$refs.editTaskItem && this.$refs.editTaskItem.contains(target);
if (isDropdown && !isEditAction) return;
if (this.$store.state.spellOptions.castingSpell) return;
if (!this.$store.state.spellOptions.castingSpell) {
if (!this.showEdit) {
this.$emit('taskSummary', task);
} else {
this.$emit('editTask', task);
}
},
@@ -1137,6 +1176,22 @@ export default {
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
},
async score (direction) {
if (this.showTaskLockIcon) return;
if (this.task.type === 'habit' && !this.task[direction]) return;
if (
this.isGroupTask && direction === 'down'
&& ['todo', 'daily'].indexOf(this.task.type) !== -1
&& !((this.task.group.completedBy && this.task.group.completedBy.userId === this.user._id)
|| (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]))
) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: this.task.group.assignedUsers[0] || this.task.group.completedBy.userId,
});
this.task.completed = false;
return;
}
if (this.isYesterdaily === true) {
await this.beforeTaskScore(this.task);
this.task.completed = !this.task.completed;
+81 -328
View File
@@ -6,7 +6,7 @@
size="sm"
:hide-footer="true"
@hidden="onClose()"
@show="handleOpen()"
@show="syncTask()"
@shown="focusInput()"
>
<div
@@ -22,7 +22,9 @@
>
{{ title }}
</h2>
<div class="ml-auto d-flex align-items-center">
<div
class="ml-auto d-flex align-items-center"
>
<button
class="cancel-task-btn mr-3"
:class="cssClass('headings')"
@@ -55,7 +57,7 @@
<div class="form-group">
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<input
@@ -66,28 +68,29 @@
type="text"
required="required"
spellcheck="true"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
>
</div>
<div
v-if="isUserTask || isChallengeTask || isOriginalChallengeTask"
class="form-group mb-0"
>
<label
class="d-flex align-items-center justify-content-between mb-1"
>
<span
:class="cssClass('headings')"
>{{ $t('notes') }}</span>
<small>
<div class="d-flex">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
:text="`${$t('notes')}`"
/>
<small
class="my-1"
>
<a
target="_blank"
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
:class="cssClass('headings')"
>{{ $t('markdownHelpLink') }}</a>
</small>
</label>
</div>
<textarea
v-model="task.notes"
class="form-control input-notes"
@@ -95,49 +98,6 @@
:placeholder="$t('addNotes')"
></textarea>
</div>
<div
v-if="showManagerNotes"
class="form-group mb-0 mt-3"
>
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage"
:text="$t('managerNotes')"
/>
<textarea
v-model="managerNotes"
class="form-control input-notes"
:class="cssClass('input')"
:placeholder="$t('addNotes')"
:disabled="groupAccessRequiredAndOnPersonalPage"
></textarea>
</div>
<div
v-if="task.group && task.group.assignedDate && !task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateOnly', {
date: formattedDate(task.group.assignedDate),
})"
>
</div>
<div
v-if="task.group && task.group.assignedDate && task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateAndUser', {
username: task.group.assigningUsername,
date: formattedDate(task.group.assignedDate),
})"
>
</div>
</div>
<div
v-if="task && groupAccessRequiredAndOnPersonalPage
&& (task.type === 'daily' || task.type === 'todo')"
class="summary-sentence py-3 px-4"
v-html="summarySentence"
>
</div>
<div
v-if="task"
@@ -181,8 +141,6 @@
>
<checklist
:items.sync="task.checklist"
:disable-items="groupAccessRequiredAndOnPersonalPage"
:disable-drag="groupAccessRequiredAndOnPersonalPage"
/>
</div>
<div
@@ -194,7 +152,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.up ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleUpDirection()"
>
<div
@@ -220,7 +178,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.down ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleDownDirection()"
>
<div
@@ -243,11 +201,11 @@
</button>
</div>
<template
v-if="task.type !== 'reward' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type !== 'reward'"
>
<div class="d-flex mt-3">
<lockable-label
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="$t('difficulty')"
/>
<div
@@ -258,13 +216,12 @@
</div>
<select-difficulty
:value="task.priority"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
@select="setDifficulty($event)"
/>
</template>
<div
v-if="task.type === 'todo' && !groupAccessRequiredAndOnPersonalPage
&& (!challengeAccessRequired || task.date)"
v-if="task.type === 'todo' && (!challengeAccessRequired || task.date)"
class="option mt-3"
>
<div class="form-group">
@@ -281,7 +238,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -297,7 +254,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -314,7 +271,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -344,8 +301,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && task.frequency === 'weekly'
&& !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily' && task.frequency === 'weekly'"
class="option mt-3"
>
<div class="form-group">
@@ -405,7 +361,7 @@
</div>
</div>
<div
v-if="isUserTask"
v-if="!groupId"
class="tags-select option mt-3"
>
<div class="tags-inline form-group row">
@@ -428,16 +384,16 @@
</div>
</div>
<div
v-if="task.type === 'habit'"
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
>
<div class="form-group">
<lockable-label
:locked="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:locked="challengeAccessRequired"
:text="$t('resetCounter')"
/>
<select-translated-array
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
:items="['daily', 'weekly', 'monthly']"
:value="task.frequency"
@select="task.frequency = $event"
@@ -448,25 +404,18 @@
v-if="groupId"
class="option group-options mt-3"
>
<div
v-if="task.type === 'todo'"
class="form-group"
>
<label
v-once
class="mb-1"
>{{ $t('sharedCompletion') }}</label>
<select-translated-array
:items="['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']"
:value="sharedCompletion"
@select="sharedCompletion = $event"
/>
</div>
<div class="form-group row mt-3 mb-3">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedTo') }}</label>
class="col-10 mb-1"
>{{ $t('assignTo') }}</label>
<a
v-if="assignedMembers.length > 0"
class="col-2 text-right mt-1"
@click="clearAssignments"
>
{{ $t('clear') }}
</a>
<div class="col-12">
<select-multi
ref="assignMembers"
@@ -479,17 +428,6 @@
/>
</div>
</div>
<div class="form-group flex-group mt-3 mb-4">
<label
v-once
class="mb-0 flex"
>{{ $t('approvalRequired') }}</label>
<toggle-switch
class="d-inline-block"
:checked="requiresApproval"
@change="updateRequiresApproval"
/>
</div>
</div>
<div
v-if="advancedSettingsAvailable"
@@ -605,9 +543,7 @@
</b-collapse>
</div>
<div
v-if="purpose !== 'create'
&& !challengeAccessRequired
&& !groupAccessRequiredAndOnPersonalPage"
v-if="purpose !== 'create' && !challengeAccessRequired"
class="d-flex justify-content-center mt-4 mb-4"
>
<button
@@ -654,6 +590,12 @@
@import '~@/assets/scss/colors.scss';
#task-modal {
a:not(.dropdown-item) {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
}
.modal-dialog.modal-sm {
max-width: 448px;
}
@@ -662,6 +604,11 @@
margin-bottom: 0;
}
.custom-control-input {
z-index: -1;
opacity: 0;
}
.modal-header {
.form-group {
margin-bottom: 1rem;
@@ -681,9 +628,6 @@
}
input, textarea {
&:not(:host-context(.tags-popup)) {
border: none;
}
transition-property: border-color, box-shadow, color, background;
background-color: rgba(255, 255, 255, 0.5);
&:focus:not(:disabled), &:active:not(:disabled), &:hover:not(:disabled) {
@@ -1021,11 +965,6 @@
}
}
.summary-sentence {
background-color: $gray-700;
line-height: 1.71;
}
.input-group-text {
font-size: 14px;
font-weight: bold;
@@ -1046,12 +985,8 @@
<script>
import axios from 'axios';
import clone from 'lodash/clone';
import keys from 'lodash/keys';
import pickBy from 'lodash/pickBy';
import moment from 'moment';
import Datepicker from '@/components/ui/datepicker';
import toggleSwitch from '@/components/ui/toggleSwitch';
import toggleCheckbox from '@/components/ui/toggleCheckbox';
import markdownDirective from '@/directives/markdown';
import { mapGetters, mapActions, mapState } from '@/libs/store';
@@ -1061,6 +996,8 @@ import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
@@ -1071,11 +1008,11 @@ import chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg';
export default {
components: {
SelectMulti,
Datepicker,
toggleSwitch,
checklist,
selectDifficulty,
selectTranslatedArray,
@@ -1085,6 +1022,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [syncTask],
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {
@@ -1102,9 +1040,6 @@ export default {
calendar: calendarIcon,
grip: gripIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
sharedCompletion: 'singleCompletion',
managerNotes: '',
members: [],
membersNameAndId: [],
memberNamesById: {},
@@ -1118,15 +1053,6 @@ export default {
per: 'perception',
},
calendarHighlights: { dates: [new Date()] },
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
};
},
computed: {
@@ -1145,7 +1071,6 @@ export default {
|| this.task.type === 'todo'
|| this.purpose === 'create'
|| !this.isUserTask
|| this.groupAccessRequiredAndOnPersonalPage
) {
return false;
}
@@ -1159,29 +1084,17 @@ export default {
return true;
},
groupAccessRequiredAndOnPersonalPage () {
if (!this.groupId && this.task.group && this.task.group.id) return true;
return false;
},
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1
&& !this.isOriginalChallengeTask
&& (!this.groupAccessRequiredAndOnPersonalPage || this.checklist.length > 0);
},
showManagerNotes () {
return Boolean(this.task.group && this.task.group.managerNotes)
|| (
!this.groupAccessRequiredAndOnPersonalPage && this.managers.indexOf(this.user._id) !== -1
);
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
isChallengeTask () {
return Boolean(this.task.challenge && this.task.challenge.id);
},
onUserPage () {
isUserTask () {
return !this.challengeId && !this.groupId;
},
challengeAccessRequired () {
return this.onUserPage && this.isChallengeTask;
return this.isUserTask && this.isChallengeTask;
},
isOriginalChallengeTask () {
const isUserChallenge = Boolean(this.task.userId);
@@ -1198,9 +1111,6 @@ export default {
const type = this.$t(this.task.type);
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type });
},
isUserTask () {
return !this.challengeId && !this.groupId;
},
repeatSuffix () {
const { task } = this;
@@ -1235,23 +1145,6 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
},
watch: {
task () {
@@ -1279,47 +1172,6 @@ export default {
createTask: 'tasks:create',
createTag: 'tags:createTag',
}),
async syncTask () {
if (this.task?.group?.managerNotes) {
this.managerNotes = this.task.group.managerNotes;
}
if (this.groupId && this.task.group?.approval) {
this.requiresApproval = this.task.group.approval.required;
}
if (this.task?.group?.sharedCompletion) {
this.sharedCompletion = this.task.group.sharedCompletion;
} else if (this.task.group) {
this.sharedCompletion = 'singleCompletion';
}
if (this.groupId) {
const members = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId,
includeAllPublicFields: true,
});
this.members = members;
this.membersNameAndId = [];
this.members.forEach(member => {
this.membersNameAndId.push({
id: member._id,
name: member.profile.name,
addlText: `@${member.auth.local.username}`,
});
this.memberNamesById[member._id] = member.profile.name;
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) {
this.assignedMembers = this.task.group.assignedUsers;
}
}
// @TODO: This whole component is mutating a prop
// and that causes issues. We need to not copy the prop similar to group modals
if (this.task) this.checklist = clone(this.task.checklist);
},
async handleOpen () {
this.syncTask();
},
cssClass (suffix) {
if (!this.task) {
return '';
@@ -1345,104 +1197,6 @@ export default {
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
calculateMonthlyRepeatDays (newRepeatsOn) {
if (!this.task) return;
const { task } = this;
@@ -1470,16 +1224,6 @@ export default {
if (!this.canSave) return;
if (this.newChecklistItem) this.addChecklistItem();
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
this.task.group.sharedCompletion = this.sharedCompletion;
this.task.managerNotes = this.managerNotes;
this.task.group.managerNotes = this.managerNotes;
}
if (this.task.type === 'reward' && this.task.value === '') {
this.task.value = 0;
}
@@ -1498,12 +1242,18 @@ export default {
tasks: [this.task],
});
Object.assign(this.task, response);
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
}));
Promise.all(promises);
this.task.group.assignedUsers = this.assignedMembers;
assignedUserIds: this.assignedMembers,
});
this.assignedMembers.forEach(memberId => {
if (!this.task.assignedUsersDetail) this.task.assignedUsersDetail = {};
this.task.assignedUsersDetail[memberId] = {
assignedDate: new Date(),
assigningUsername: this.user.auth.local.username,
completed: false,
};
});
this.$emit('taskCreated', this.task);
} else {
this.createTask(this.task);
@@ -1525,22 +1275,14 @@ export default {
this.$root.$emit('bv::hide::modal', 'task-modal');
},
onClose () {
if (this.task.group && this.task.group.managerNotes) this.managerNotes = null;
this.newChecklistItem = '';
this.$emit('cancel');
},
updateRequiresApproval (newValue) {
let truthy = true;
if (!newValue) truthy = false; // This return undefined instad of false
this.requiresApproval = truthy;
},
async toggleAssignment (memberId) {
if (this.purpose === 'create') {
return;
}
const assignedIndex = this.assignedMembers.indexOf(memberId);
if (assignedIndex === -1) {
await this.$store.dispatch('tasks:unassignTask', {
taskId: this.task._id,
@@ -1549,10 +1291,21 @@ export default {
} else {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
assignedUserIds: [memberId],
});
}
},
async clearAssignments () {
if (this.purpose === 'edit') {
for (const assignedMember of this.assignedMembers) {
await this.$store.dispatch('tasks:unassignTask', { // eslint-disable-line no-await-in-loop
taskId: this.task._id,
userId: assignedMember,
});
}
}
this.assignedMembers = [];
},
focusInput () {
this.$refs.inputToFocus.focus();
},
@@ -0,0 +1,358 @@
<template>
<b-modal
id="task-summary"
:hide-footer="true"
@hidden="$emit('cancel')"
>
<div
v-if="task"
slot="modal-header"
class="task-modal-header px-4 d-flex align-items-center"
:class="cssClass('bg')"
>
<h2
class="my-auto"
:class="cssClass('headings')"
>
{{ title }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
:class="cssClass('headings')"
aria-hidden="true"
tabindex="0"
@click="cancel()"
@keypress.enter="cancel()"
v-html="icons.close"
></div>
</div>
<div
v-if="task"
class="task-modal-content pt-3 px-4 pb-4"
>
<div class="summary-block">
<h3> {{ $t('title') }} </h3>
<p> {{ task.text }} </p>
</div>
<div
v-if="task.notes"
class="summary-block"
>
<h3> {{ $t('notes') }} </h3>
<p> {{ task.notes }} </p>
</div>
<div class="summary-block">
<h3> {{ $t('description') }} </h3>
<p v-html="summarySentence" ></p>
</div>
<div
v-if="task.checklist && task.checklist.length > 0"
class="summary-block"
>
<checklist
:items.sync="task.checklist"
:disableDrag="true"
:disableEdit="true"
/>
</div>
<div
class="summary-block"
v-if="assignedUsernames.length > 0"
>
<h3> {{ $t('assignedTo') }} </h3>
<div
class="d-flex flex-wrap"
>
<span
v-for="member in assignedUsernames"
:key="member"
class="assigned-member py-1 px-75 mb-1 mr-1"
>
@{{ member }}
</span>
</div>
</div>
</div>
<div
v-if="task && task.group && task.group.assignedUsersDetail
&& task.group.assignedUsersDetail[user._id]"
class="assignment-footer text-center py-2"
v-html="$t('assignedDateAndUser', {
username: task.group.assignedUsersDetail[user._id].assigningUsername,
date: formattedDate(task.group.assignedUsersDetail[user._id].assignedDate),
})"
>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#task-summary {
overflow-y: hidden;
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
.modal-dialog {
width: 448px;
margin-top: 50vh;
transform: translateY(-50%);
}
.modal-header {
padding: 0px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.assigned-member {
border: 1px solid $gray-400;
border-radius: 100px;
color: $gray-100;
font-size: 12px;
line-height: 1.33;
}
.assignment-footer {
color: $gray-100;
background-color: $gray-700;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.close-x {
height: 16px;
width: 16px;
position: relative;
opacity: 0.75;
cursor: pointer;
&:hover, &:focus {
opacity: 1;
}
}
.summary-block:not(:last-of-type) {
margin-bottom: 1rem;
}
.task-modal-content {
h3 {
margin-bottom: 0.25rem;
}
p {
margin-bottom: 0px;
}
h3, p {
color: $gray-50;
}
}
.task-modal-header {
color: $white;
height: 60px;
width: 100%;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
h2 {
color: $white;
}
}
</style>
<script>
import keys from 'lodash/keys';
import moment from 'moment';
import pickBy from 'lodash/pickBy';
import checklist from './modal-controls/checklist';
import { mapGetters, mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
export default {
components: {
checklist,
},
props: ['task'],
data () {
return {
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
icons: Object.freeze({
close: closeIcon,
}),
};
},
computed: {
...mapGetters({
getTaskClasses: 'tasks:getTaskClasses',
}),
...mapState({
user: 'user.data',
}),
assignedUsernames () {
if (!this.task.group || !this.task.group.assignedUsers
|| !this.task.group.assignedUsersDetail) return [];
const usernames = [];
for (const user of this.task.group.assignedUsers) {
usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername);
}
return usernames;
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
title () {
const type = this.$t(this.task.type);
return this.$t('taskSummary', { type });
},
},
methods: {
cancel () {
this.$root.$emit('bv::hide::modal', 'task-summary');
},
cssClass (suffix) {
if (!this.task) {
return '';
}
return this.getTaskClasses(this.task, `edit-modal-${suffix}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
},
};
</script>
+48 -33
View File
@@ -1,5 +1,8 @@
<template>
<div class="row user-tasks-page">
<div
class="row user-tasks-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<broken-task-modal />
<task-modal
ref="taskModal"
@@ -7,6 +10,11 @@
:purpose="creatingTask !== null ? 'create' : 'edit'"
@cancel="cancelTaskModal()"
/>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="col-12">
<div class="row tasks-navigation">
<div class="col-12 col-md-4 offset-md-4">
@@ -160,45 +168,43 @@
</div>
</div>
</div>
<div class="create-task-area d-flex">
<transition name="slide-tasks-btns">
<div class="create-task-area">
<div
id="create-task-btn"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
v-if="openCreateBtn"
class="d-flex"
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</div>
<div
v-if="openCreateBtn"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon"
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('addTask') }}
</b-tooltip>
</div>
</div>
<div class="row tasks-columns">
@@ -211,6 +217,7 @@
:search-text="searchTextThrottled"
:selected-tags="selectedTags"
@editTask="editTask"
@taskSummary="taskSummary"
@openBuyDialog="openBuyDialog($event)"
/>
</div>
@@ -345,7 +352,7 @@
}
.create-task-area {
top: -2.5rem;
top: 1px;
}
.drag {
@@ -381,6 +388,7 @@ import cloneDeep from 'lodash/cloneDeep';
import draggable from 'vuedraggable';
import TaskColumn from './column';
import TaskModal from './taskModal';
import TaskSummary from './taskSummary';
import spells from './spells';
import markdown from '@/directives/markdown';
@@ -401,6 +409,7 @@ export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
spells,
brokenTaskModal,
draggable,
@@ -524,13 +533,19 @@ export default {
};
this.newTag = null;
},
// Need Vue.nextTick() otherwise the first time the modal is not rendered
editTask (task) {
this.editingTask = cloneDeep(task);
// Necessary otherwise the first time the modal is not rendered
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
createTask (type) {
this.openCreateBtn = false;
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
@@ -84,6 +84,7 @@
import moment from 'moment';
import { mapState } from '@/libs/store';
import scoreTask from '@/mixins/scoreTask';
import sync from '@/mixins/sync';
import Task from './task';
import LoadingSpinner from '../ui/loadingSpinner';
@@ -92,7 +93,7 @@ export default {
Task,
LoadingSpinner,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
props: {
yesterDailies: {
type: Array,
@@ -180,6 +181,7 @@ export default {
this.isLoading = false;
this.$root.$emit('bv::hide::modal', 'yesterdaily');
if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync();
},
},
};
@@ -126,7 +126,7 @@
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 offset-md-3 text-center nav">
<div class="text-center nav">
<div
class="nav-item"
:class="{active: selectedPage === 'profile'}"
@@ -470,6 +470,7 @@
.gift-icon svg {
height: 14px;
}
</style>
<style lang="scss" scoped>
@@ -538,6 +539,7 @@
}
.nav {
width: 100%;
font-weight: bold;
min-height: 40px;
justify-content: center;
@@ -710,6 +712,27 @@
}
}
}
@media (max-width: 990px) {
.profile-actions {
flex-direction: column;
}
.profile-actions :not(:last-child) {
margin-bottom: 15px;
}
.profile-actions {
margin-right: 0;
}
}
@media (max-width: 550px) {
.member-details {
flex-direction: column;
}
.member-details .avatar {
margin-bottom: 15px;
}
}
</style>
<script>
@@ -969,7 +992,8 @@ export default {
axios.post(`/api/v4/user/block/${this.user._id}`);
},
openSendGemsModal () {
this.$root.$emit('habitica::send-gems', this.user);
this.$store.state.giftModalOptions.startingPage = 'buyGems';
this.$root.$emit('habitica::send-gift', this.user);
},
adminTurnOnShadowMuting () {
if (!this.hero.flags) {
@@ -4,7 +4,7 @@
class="standard-page"
>
<div class="row">
<div class="col-12 col-md-6">
<div class="stats-section-equipment col-12 col-md-6">
<h2 class="text-center">
{{ $t('equipment') }}
</h2>
@@ -12,7 +12,7 @@
<div
v-for="(label, key) in equipTypes"
:key="key"
class="col-12 col-md-4 item-wrapper"
class="item-wrapper"
>
<div
v-if="label !== 'skip'"
@@ -48,7 +48,7 @@
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="stats-section-costume col-12 col-md-6">
<h2 class="text-center">
{{ $t('costume') }}
</h2>
@@ -57,7 +57,7 @@
<div
v-for="(label, key) in equipTypes"
:key="key"
class="col-12 col-md-4 item-wrapper"
class="item-wrapper"
>
<!-- Append a "C" to the key name since HTML IDs have to be unique.-->
<div
@@ -111,7 +111,7 @@
</div>
</div>
<div class="row pet-mount-row">
<div class="col-12 col-md-6">
<div class="stats-section-pets col-12 col-md-6">
<h2
v-once
class="text-center"
@@ -119,8 +119,7 @@
{{ $t('pets') }}
</h2>
<div class="well pet-mount-well">
<div class="row col-12">
<div class="col-12 col-md-4">
<div class="pet-mount-well-image">
<div
class="box"
:class="{white: user.items.currentPet}"
@@ -131,7 +130,7 @@
></div>
</div>
</div>
<div class="col-12 col-md-8">
<div class="pet-mount-well-text">
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
<div>
<strong>{{ $t('petsFound') }}:</strong>
@@ -142,10 +141,9 @@
{{ beastMasterProgress(user.items.pets) }}
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="stats-section-mounts col-12 col-md-6">
<h2
v-once
class="text-center"
@@ -153,28 +151,26 @@
{{ $t('mounts') }}
</h2>
<div class="well pet-mount-well">
<div class="row col-12">
<div class="col-12 col-md-4">
<div class="pet-mount-well-image">
<div
class="box"
:class="{white: user.items.currentMount}"
>
<div
class="box"
:class="{white: user.items.currentMount}"
>
<div
class="mount"
:class="`Mount_Icon_${user.items.currentMount}`"
></div>
</div>
class="mount"
:class="`Mount_Icon_${user.items.currentMount}`"
></div>
</div>
<div class="col-12 col-md-8">
<div>{{ formatAnimal(user.items.currentMount, 'mount') }}</div>
<div>
<strong>{{ $t('mountsTamed') }}:</strong>
<span>{{ totalCount(user.items.mounts) }}</span>
</div>
<div>
<strong>{{ $t('mountMasterProgress') }}:</strong>
<span>{{ mountMasterProgress(user.items.mounts) }}</span>
</div>
</div>
<div class="pet-mount-well-text">
<div>{{ formatAnimal(user.items.currentMount, 'mount') }}</div>
<div>
<strong>{{ $t('mountsTamed') }}:</strong>
<span>{{ totalCount(user.items.mounts) }}</span>
</div>
<div>
<strong>{{ $t('mountMasterProgress') }}:</strong>
<span>{{ mountMasterProgress(user.items.mounts) }}</span>
</div>
</div>
</div>
@@ -309,15 +305,13 @@
v-if="showStatsSave"
class="row save-row"
>
<div class="col-12 col-md-6 offset-md-3 text-center">
<button
class="btn btn-primary"
:disabled="loading"
@click="saveAttributes()"
>
{{ loading ? $t('loading') : $t('save') }}
</button>
</div>
<button
class="btn btn-primary"
:disabled="loading"
@click="saveAttributes()"
>
{{ loading ? $t('loading') : $t('save') }}
</button>
</div>
</div>
</div>
@@ -650,10 +644,17 @@ export default {
border-radius: 2px;
padding: 0.4em;
padding-top: 1em;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 15px;
}
.well.pet-mount-well {
padding-left: 15px;
padding-bottom: 1em;
flex-wrap: nowrap;
justify-content: flex-start;
strong {
margin-right: .2em;
@@ -690,12 +691,13 @@ export default {
}
.save-row {
margin-top: 1em;
margin: 2em 0 1em 0;
justify-content: center;
}
.gear.box {
vertical-align: top;
margin: 0 auto;
// margin: 0 auto;
}
.gear-label {
@@ -721,4 +723,34 @@ export default {
// breaks the long words without a space
word-break: break-word;
}
@media (max-width: 850px) {
#stats .col-md-6 {
flex: none;
max-width: 100%;
}
}
@media(max-width: 990px) {
.modal-body #stats .col-md-6 {
flex: none;
max-width: 100%;
}
[class^="stats-section-"] {
margin-bottom: 30px;
}
#allocation {
.box {
width: 100%;
height: 100%;
.col-9 {
padding: 0;
margin: 0;
}
.col-9 div:first-child {
font-size: 13px;
}
}
}
}
</style>
+10
View File
@@ -29,6 +29,11 @@ export default [
type: 'Staff',
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
},
{
name: 'heyeilatan',
type: 'Staff',
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
},
{
name: 'Alys',
type: 'Moderator',
@@ -39,6 +44,11 @@ export default [
type: 'Moderator',
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
},
{
name: 'deilann',
type: 'Moderator',
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
},
{
name: 'Dewines',
type: 'Moderator',
+2 -18
View File
@@ -15,24 +15,8 @@ export default {
}),
},
methods: {
async beforeTaskScore (task) {
const { user } = this;
if (this.castingSpell) return false;
if (task.group.approval.required && !task.group.approval.approved) {
task.group.approval.requested = true;
const { data: groupPlans } = await this.$store.dispatch('guilds:getGroupPlans');
const groupPlan = groupPlans.find(g => g.id === task.group.id);
if (groupPlan) {
const managers = Object.keys(groupPlan.managers);
managers.push(groupPlan.leader);
if (managers.indexOf(user._id) !== -1) {
task.group.approval.approved = true;
}
}
}
return true;
async beforeTaskScore () {
return (!this.castingSpell);
},
playTaskScoreSound (task, direction) {
switch (task.type) { // eslint-disable-line default-case
+32
View File
@@ -0,0 +1,32 @@
import clone from 'lodash/clone';
export default {
methods: {
async syncTask () {
if (this.groupId || this.task?.group.id) {
const members = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId || this.task?.group.id,
includeAllPublicFields: true,
});
this.members = members;
this.membersNameAndId = [];
this.members.forEach(member => {
this.membersNameAndId.push({
id: member._id,
name: member.profile.name,
addlText: `@${member.auth.local.username}`,
});
this.memberNamesById[member._id] = member.profile.name;
});
this.assignedMembers = [];
if (this.task?.group?.assignedUsers) {
this.assignedMembers = this.task.group.assignedUsers;
}
}
// @TODO: Task modal component is mutating a prop
// and that causes issues. We need to not copy the prop similar to group modals
if (this.task) this.checklist = clone(this.task.checklist);
},
},
};

Some files were not shown because too many files have changed in this diff Show More